From 3f65ec55fc72af46daab9a8eadab32ba813dc6f0 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Thu, 5 Dec 2024 20:40:13 +0100 Subject: [PATCH 01/91] Add Taskjuggler project file --- kontor.tjp | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 kontor.tjp diff --git a/kontor.tjp b/kontor.tjp new file mode 100644 index 0000000..a5d46c6 --- /dev/null +++ b/kontor.tjp @@ -0,0 +1,30 @@ +project kontor "Kontor" "0.1.0" 2024-12-05 +5m { + timezone "Europe/Berlin" + timeformat "%d.%m.%Y" + numberformat "-" "" "" "," 1 + currencyformat "-" "" "" "," 0 + currency "EUR" + + scenario plan "Plan" { + scenario real "Realität" + } +} + +resource gcpce "Google Cloud Compute Engine" { + efficiency 0.0 + rate 0.25 +} + +task flask "Kontor-Flask" { + task import "Import repository kontor-flask into directory flask" +} +task springboot "Springboot Vaadin" { + task import "Import repository kontor-spring into directory springboot" +} + +taskreport "Arbeitsliste" { + formats html + hidetask ~isleaf() + sorttasks plan.end.up +} + From 66d61e2c1f41a7c1ecfbd3eca81beed9c3fca0b0 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Wed, 8 Jan 2025 21:08:15 +0100 Subject: [PATCH 02/91] add entity token --- LICENSE | 9 +++++ .../de/thpeetz/kontor/admin/data/Token.java | 35 +++++++++++++++++++ .../de/thpeetz/kontor/admin/data/User.java | 8 ++--- 3 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 LICENSE create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/Token.java diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7cfa22c --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2024 Kontor + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/Token.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/Token.java new file mode 100644 index 0000000..8e77f90 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/Token.java @@ -0,0 +1,35 @@ +package de.thpeetz.kontor.admin.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import java.util.Date; + +@Slf4j +@Getter +@Setter +@ToString +@Entity +@Table(indexes = @Index(columnList = "token"), uniqueConstraints = @UniqueConstraint(columnNames = {"token"})) +public class Token extends AbstractEntity { + + private String token; + + private Date lastUsedDate; + + private boolean enabled; + + @ManyToOne + @JoinColumn(name="user_id") + @NotNull + private User user; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/User.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/User.java index a80b6bc..ec16233 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/data/User.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/User.java @@ -38,14 +38,14 @@ public class User extends AbstractEntity { private boolean enabled; - private boolean tokenExpired; - - private String token; - @OneToMany(fetch = FetchType.EAGER, mappedBy = "user") @Nullable private List matrix = new LinkedList<>(); + @OneToMany(fetch = FetchType.EAGER, mappedBy = "user") + @Nullable + private List tokens = new LinkedList<>(); + public String getFullName() { StringBuilder fullNamBuilder = new StringBuilder(); if (firstName != null) { From 57e7b9e999389275133d3323bfbb5e6df32c1832 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 5 Jan 2025 14:10:15 +0100 Subject: [PATCH 03/91] first implementation to show Comics and MediaFiles --- .gitignore | 5 ++ gui/application-export.png | Bin 0 -> 513 bytes gui/application-import.png | Bin 0 -> 524 bytes gui/arrow-circle-double.png | Bin 0 -> 836 bytes gui/comic_model.py | 89 ++++++++++++++++++++++++ gui/cross.png | Bin 0 -> 544 bytes gui/kontor.py | 131 ++++++++++++++++++++++++++++++++++++ gui/media_file_model.py | 91 +++++++++++++++++++++++++ gui/resources.py | 7 ++ gui/tick.png | Bin 0 -> 634 bytes 10 files changed, 323 insertions(+) create mode 100644 .gitignore create mode 100644 gui/application-export.png create mode 100644 gui/application-import.png create mode 100644 gui/arrow-circle-double.png create mode 100644 gui/comic_model.py create mode 100644 gui/cross.png create mode 100644 gui/kontor.py create mode 100644 gui/media_file_model.py create mode 100644 gui/resources.py create mode 100644 gui/tick.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45f246d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__ +.idea/ +bonus +icons +icons-shadowless diff --git a/gui/application-export.png b/gui/application-export.png new file mode 100644 index 0000000000000000000000000000000000000000..555887a28d64bc812c4dfa98a6ff1da1927b7792 GIT binary patch literal 513 zcmV+c0{;DpP)A0|fCeqz@KJVX8nnt2D9k2UrOajFq_SwR4OUbbUJ;HgeDQF66oE777mBiWdd2F(NKOO zk$Ax17hTui{#6jPXfz7dY87I!*bgRr4TVA=afmd=7~nkYBq$b(su#^>Q?2y;%rJ~= zA;j4sgM^B|5YOb(M4cc`5q!^h_wPg5is0Dq{42l!6{r`}{QE*r00000NkvXXu0mjf Djqud07&@TR)18#v5Hqj8@|Bnpn>z;V`CueWd< z7oI05i2^A#D9Qz9v#hs`;>*D7{eB7>0ppH2SLstJO-8BwR~M`A7kF ztyTlI66tgrtyT-MSPa~yU>3AuKR54&OXJih@;oco-=1sDLQa}`@LC7Iy> O0000dz zNLw#yfl;S$OS+NxP!T0`q27a_KcR=-dJ5{Xm+WEf!J>k|BqE5#C}ImGwQ$$<(wdh& z>AIJ5zVFx9lo8J{@aWY%oIP6CAZKYHol)uRf|554m_YTnpAlGl zL9{5M4)qVt#u>|mLQVrh46q|<)MP|~t8EE-8crSv>~io{4+abW2A~lV0z>OM)mbBD zmWtqV;+?9YGlhpQQ@M9zi{*KRG_T5PQ_nvC9tMo_mT6XEIU5;J+S?d(HD_0f!1ES7 z7@Uj?4!kD>&?r8e&c1jan^9Cs!}3z0d7Izafj)-}rbYZR}EdihS6`M#Fcy= zaw{u#mV;BNWe3{Sqiq;{_M>cOP*8MhP1CLQf3t|C2tpM55ycYh7RL%}Yf+Bp#e!~C z2(WG^ZnlxC>igv!sW2GM8SslVJ(oFwkp~5*6E^0rT$Q zya}^2lB>ITrX0zGEa!lc90$HC4~#^g#%ZB2j3vzHhi;TO$n{?Wwx(^0>^QA_x~H^z zTf=2SxY0rCf|fl@rb7TXYr%pE(0=u@G literal 0 HcmV?d00001 diff --git a/gui/comic_model.py b/gui/comic_model.py new file mode 100644 index 0000000..2472670 --- /dev/null +++ b/gui/comic_model.py @@ -0,0 +1,89 @@ +from datetime import datetime + +import mariadb +from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt +from PySide6.QtGui import QColor + + +class ComicTableModel(QAbstractTableModel): + + def __init__(self, db_config, main_window): + super().__init__() + self.main_window = main_window + self._data = [] + self.status_bar = main_window.statusBar + self.mariadb_conn = mariadb.connect( + host=db_config['mariadb']['host'], + port=db_config['mariadb']['port'], + user=db_config['mariadb']['user'], + password=db_config['mariadb']['password'], + database=db_config['mariadb']['database'] + ) + self.refresh() + + def refresh(self): + data = [] + cursor = self.mariadb_conn.cursor() + cursor.execute("SELECT id, created_date, last_modified_date, title, publisher_id FROM comic") + rows = cursor.fetchall() + for row in rows: + data.append(list(row)) + self.status_bar.showMessage(f"{len(rows)} Einträge geladen", 3000) + self._data = data + + def rowCount(self, parent=QModelIndex()): + # The length of the outer list. + return len(self._data) + + def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): + if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: + match col: + case 0: + return "ID" + case 1: + return "Created" + case 2: + return "Updated" + case 3: + return "Title" + case 4: + return "Verlag" + if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: + return str(col + 1) + + def data(self, index, role=Qt.ItemDataRole.DisplayRole): + value = self._data[index.row()][index.column()] + if role == Qt.ItemDataRole.DisplayRole: + if isinstance(value, datetime): + return value.strftime("%Y-%m-%d %M:%M:%S") + if isinstance(value, str): + return value + if isinstance(value, bytes): + if value == b'\x01': + return "True" + return "False" + return value + if role == Qt.ItemDataRole.DecorationRole: + if isinstance(value, bytes): + # print('{}: {}'.format(value, type(value))) + if value == b'\x01': + return self.main_window.tick + else: + return self.main_window.cross + + + def columnCount(self, index=QModelIndex()): + # The following takes the first sub-list, and returns + # the length (only works if all rows are an equal length) + return len(self._data[0]) + + def setData(self, index, value, role=Qt.ItemDataRole.EditRole): + if role == Qt.ItemDataRole.EditRole: + self._data[index.row()][index.column()] = value + if role == Qt.ItemDataRole.CheckStateRole: + checked = value == Qt.CheckState.Checked + self._data[index.row()][index.column()] = checked + return True + + def flags(self, index): + return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate diff --git a/gui/cross.png b/gui/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..6b9fa6dd36ee8165272a13dd263f573507c78ca6 GIT binary patch literal 544 zcmV+*0^j|KP)L-ku(! z6_?D-?!0+#=VtbVZQGdTnZt~apI_HPK#=zVadHM((E`e*C+RoFb)Qo8-U{Lb7{`Tz zw4B8FZ|o?Wox+p=1wp47C;7ar*Xu~;a?<=sjPv>+otDjJ6FaGt!YnNyxQQhp)G1V! zk;r6ZtyV)c8pTbiRAnHRNXTxti%=+p$4aG2*+mMM&xrdiU^`VPk^N*+HX98^7!HT% zwA%;A{T)GxeQ|RcxyzV%! z$AbYD+)i1R64zB?q}NmTz}5|04~J#YG_goA*Eq(QJvp7pG14hUj1pZ^t>3S*xqHUO ze~r;}zTY_XkZ*}d@gm!;N90h8nBQenBUZ_ukZKNicn*hc_Pmc!Jn|2=s<~}>!Sb>Qm3!QP46b_JFw5YU70Tu$X}=T}ez@@t%j iG9vD)nDux55?}x$+|UyQVK_bj0000tYd4K$mX5uyr2F@fdffp{&DSHtl4|Bn9=ukpCx`#%PTUr-D(@b7;C zhJXJjrnw{;1KBM=6&|E`fsNrGL!Y6%zUh}QUl`(@V)PmQFtotEKmafTHP0uP}7&%m4pcKQ#X)BiAJ2y+PrtBI>9eEIt2-_c7)?*Lsf z5vX5*Af@m-wBJRV%#Fiz=BdPr0!2^a2d-yv7gSU);Z6{`~Oy?E5qSnHUln4*Y%!){F!Y1~5WXoC44g zb7l_)X~q^V6MmWR7e3wj|L|Wb!|^}Y86N$^h+kv_Kw-fP!~#If$6(B4$)LlK#&G6; zC&ShMSAb$5tA9Xg5dOsg@-z^@3;;QS9f&!gG&3|;{Da~@Q2ZB({s%XJ5&#fj0J|In U+D>(nEdT%j07*qoM6N<$g0@&8X#fBK literal 0 HcmV?d00001 From 9dcc09c586982a072bfe57eca102ed01f19abde4 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 5 Jan 2025 14:13:15 +0100 Subject: [PATCH 04/91] display tick and cross for boolean values --- gui/media_file_model.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gui/media_file_model.py b/gui/media_file_model.py index 619ce2e..f06d94f 100644 --- a/gui/media_file_model.py +++ b/gui/media_file_model.py @@ -60,11 +60,11 @@ class MediaFileTableModel(QAbstractTableModel): return value.strftime("%Y-%m-%d %M:%M:%S") if isinstance(value, str): return value -# if isinstance(value, bytes): -# if value == b'\x01': -# return self.main_window.tick -# else: -# return self.main_window.cross + if isinstance(value, bytes): + if value == b'\x01': + return self.main_window.tick + else: + return self.main_window.cross return value if role == Qt.ItemDataRole.DecorationRole: if isinstance(value, bytes): From d98dc79cf8326f43daccae5f692e0cd163c4a844 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 5 Jan 2025 21:47:27 +0100 Subject: [PATCH 05/91] refresh table when updated --- gui/kontor.py | 27 ++++++++++++++----- gui/media_file_model.py | 36 +++++++++++++++++++++----- gui/{ => res}/application-export.png | Bin gui/{ => res}/application-import.png | Bin gui/{ => res}/arrow-circle-double.png | Bin gui/{ => res}/cross.png | Bin gui/{ => res}/tick.png | Bin 7 files changed, 50 insertions(+), 13 deletions(-) rename gui/{ => res}/application-export.png (100%) rename gui/{ => res}/application-import.png (100%) rename gui/{ => res}/arrow-circle-double.png (100%) rename gui/{ => res}/cross.png (100%) rename gui/{ => res}/tick.png (100%) diff --git a/gui/kontor.py b/gui/kontor.py index a435fe6..be15c66 100644 --- a/gui/kontor.py +++ b/gui/kontor.py @@ -6,8 +6,7 @@ from pathlib import Path import yaml from PySide6.QtGui import QAction, QIcon -from PySide6.QtSql import QSqlDatabase -from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView +from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView, QHBoxLayout, QCheckBox from PySide6.QtWidgets import QApplication, QLabel, QMainWindow from platformdirs import PlatformDirs @@ -21,11 +20,11 @@ class MainWindow(QMainWindow): def __init__(self, config): super().__init__() - self.tick = QIcon('tick.png') - self.cross = QIcon('cross.png') - self.import_icon = QIcon("application-import.png") - self.export_icon = QIcon("application-export.png") - self.circle_icon = QIcon("arrow-circle-double.png") + self.tick = QIcon('res/tick.png') + self.cross = QIcon('res/cross.png') + self.import_icon = QIcon("res/application-import.png") + self.export_icon = QIcon("res/application-export.png") + self.circle_icon = QIcon("res/arrow-circle-double.png") self.setWindowTitle("Kontor") self.setMinimumSize(800, 500) @@ -35,6 +34,7 @@ class MainWindow(QMainWindow): self._createStatusBar() self.data = [] + self.filter = {} self.central_widget = QWidget() parent_layout = QVBoxLayout() self.central_widget.setLayout(parent_layout) @@ -111,11 +111,24 @@ class MainWindow(QMainWindow): def generate_tab_media_file(self, db_configuration): media_file_tab = QWidget() layout = QVBoxLayout() + filter_layout = QHBoxLayout() + download_checkbox = QCheckBox() + download_checkbox.setText("Download") + download_checkbox.checkStateChanged.connect(self.refresh) + self.filter["download"] = download_checkbox + review_checkbox = QCheckBox() + review_checkbox.setText("Review") + review_checkbox.checkStateChanged.connect(self.refresh) + self.filter["review"] = review_checkbox + filter_layout.addWidget(review_checkbox) + filter_layout.addWidget(download_checkbox) + filter_layout.addStretch() model = MediaFileTableModel(db_configuration, self) self.data.append(model) media_file_tab.setLayout(layout) table_view = QTableView() table_view.setModel(model) + layout.addLayout(filter_layout) layout.addWidget(table_view) return media_file_tab diff --git a/gui/media_file_model.py b/gui/media_file_model.py index f06d94f..e34ad26 100644 --- a/gui/media_file_model.py +++ b/gui/media_file_model.py @@ -11,7 +11,6 @@ class MediaFileTableModel(QAbstractTableModel): super().__init__() self.main_window = main_window self._data = [] - self.status_bar = main_window.statusBar self.mariadb_conn = mariadb.connect( host=db_config['mariadb']['host'], port=db_config['mariadb']['port'], @@ -24,12 +23,37 @@ class MediaFileTableModel(QAbstractTableModel): def refresh(self): data = [] cursor = self.mariadb_conn.cursor() - cursor.execute("SELECT id, url, review, should_download, file_name, cloud_link FROM media_file") + filter_rule = "" + print(self.main_window.filter["download"].isChecked()) + if self.main_window.filter["download"].isChecked(): + print(self.main_window.filter["download"].isChecked()) + filter_rule = "WHERE should_download is true" + if self.main_window.filter["review"].isChecked(): + if len(filter_rule) > 0: + filter_rule += " AND " + else: + filter_rule += "WHERE " + filter_rule += "review is true" + print(f"{filter_rule=}") + try: + cursor.execute(f"SELECT id, url, review, should_download, file_name, cloud_link FROM media_file {filter_rule}") + except mariadb.Error as error: + print(error) rows = cursor.fetchall() - for row in rows: - data.append(list(row)) - self.status_bar.showMessage(f"{len(rows)} Einträge geladen", 3000) - self._data = data + print(len(rows)) + if len(rows) > 0: + self.beginResetModel() + for row in rows: + data.append(list(row)) + self._data = data + self.endResetModel() + else: + self._data = None + try: + self.layoutChanged.emit() + except: + + self.main_window.statusBar.showMessage(f"{len(rows)} Einträge geladen", 3000) def rowCount(self, parent=QModelIndex()): # The length of the outer list. diff --git a/gui/application-export.png b/gui/res/application-export.png similarity index 100% rename from gui/application-export.png rename to gui/res/application-export.png diff --git a/gui/application-import.png b/gui/res/application-import.png similarity index 100% rename from gui/application-import.png rename to gui/res/application-import.png diff --git a/gui/arrow-circle-double.png b/gui/res/arrow-circle-double.png similarity index 100% rename from gui/arrow-circle-double.png rename to gui/res/arrow-circle-double.png diff --git a/gui/cross.png b/gui/res/cross.png similarity index 100% rename from gui/cross.png rename to gui/res/cross.png diff --git a/gui/tick.png b/gui/res/tick.png similarity index 100% rename from gui/tick.png rename to gui/res/tick.png From c6d1e4d7e7e64c3eff2f0473b93a87230690110d Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 5 Jan 2025 23:44:29 +0100 Subject: [PATCH 06/91] fix errors when displaying empty table --- gui/media_file_model.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gui/media_file_model.py b/gui/media_file_model.py index e34ad26..e5af2a4 100644 --- a/gui/media_file_model.py +++ b/gui/media_file_model.py @@ -35,10 +35,7 @@ class MediaFileTableModel(QAbstractTableModel): filter_rule += "WHERE " filter_rule += "review is true" print(f"{filter_rule=}") - try: - cursor.execute(f"SELECT id, url, review, should_download, file_name, cloud_link FROM media_file {filter_rule}") - except mariadb.Error as error: - print(error) + cursor.execute(f"SELECT id, url, review, should_download, file_name, cloud_link FROM media_file {filter_rule}") rows = cursor.fetchall() print(len(rows)) if len(rows) > 0: @@ -49,14 +46,13 @@ class MediaFileTableModel(QAbstractTableModel): self.endResetModel() else: self._data = None - try: - self.layoutChanged.emit() - except: - + self.layoutChanged.emit() self.main_window.statusBar.showMessage(f"{len(rows)} Einträge geladen", 3000) def rowCount(self, parent=QModelIndex()): # The length of the outer list. + if self._data is None: + return 0 return len(self._data) def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): @@ -78,6 +74,8 @@ class MediaFileTableModel(QAbstractTableModel): return str(col+1) def data(self, index, role=Qt.ItemDataRole.DisplayRole): + if self._data is None: + return None value = self._data[index.row()][index.column()] if role == Qt.ItemDataRole.DisplayRole: if isinstance(value, datetime): @@ -101,6 +99,8 @@ class MediaFileTableModel(QAbstractTableModel): def columnCount(self, index=QModelIndex()): # The following takes the first sub-list, and returns # the length (only works if all rows are an equal length) + if self._data is None: + return 5 return len(self._data[0]) def setData(self, index, value, role=Qt.ItemDataRole.EditRole): From 3aed8af868e07657d5dade309a0b9a147486c73b Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 6 Jan 2025 17:07:20 +0100 Subject: [PATCH 07/91] implement generic table model implement generic table model which reads table info from db and constructs table view --- gui/kontor.py | 40 ++++------ gui/model_config.py | 82 +++++++++++++++++++++ gui/{media_file_model.py => table_model.py} | 71 +++++------------- 3 files changed, 117 insertions(+), 76 deletions(-) create mode 100644 gui/model_config.py rename gui/{media_file_model.py => table_model.py} (52%) diff --git a/gui/kontor.py b/gui/kontor.py index be15c66..3b601ad 100644 --- a/gui/kontor.py +++ b/gui/kontor.py @@ -6,17 +6,17 @@ from pathlib import Path import yaml from PySide6.QtGui import QAction, QIcon -from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView, QHBoxLayout, QCheckBox +from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView from PySide6.QtWidgets import QApplication, QLabel, QMainWindow from platformdirs import PlatformDirs from comic_model import ComicTableModel -from media_file_model import MediaFileTableModel +from model_config import KontorModelConfig +from table_model import KontorTableModel class MainWindow(QMainWindow): - def __init__(self, config): super().__init__() @@ -28,10 +28,10 @@ class MainWindow(QMainWindow): self.setWindowTitle("Kontor") self.setMinimumSize(800, 500) - self._createActions() - self._createMenuBar() - self._createToolBars() - self._createStatusBar() + self._create_actions() + self._create_menubar() + self._create_toolbars() + self._create_statusbar() self.data = [] self.filter = {} @@ -47,7 +47,7 @@ class MainWindow(QMainWindow): self.setCentralWidget(self.central_widget) - def _createActions(self): + def _create_actions(self): self.newAction = QAction("&New", self) self.aboutAction = QAction("&Über...", self) self.aboutAction.triggered.connect(self.about) @@ -59,7 +59,7 @@ class MainWindow(QMainWindow): self.exitAction.setShortcut("Alt+F4") self.exitAction.triggered.connect(self.close) - def _createMenuBar(self): + def _create_menubar(self): menu_bar = self.menuBar() # File menu file_menu = QMenu("&Datei") @@ -75,14 +75,14 @@ class MainWindow(QMainWindow): menu_bar.addMenu(help_menu) help_menu.addAction(self.aboutAction) - def _createToolBars(self): + def _create_toolbars(self): # Kontor toolbar kontor_tool_bar = self.addToolBar("Kontor") kontor_tool_bar.addAction(self.importAction) kontor_tool_bar.addAction(self.exportAction) kontor_tool_bar.addAction(self.refreshAction) - def _createStatusBar(self): + def _create_statusbar(self): self.statusBar = self.statusBar() self.statusBar.showMessage("Kontor ready", 6000) self.status_label = QLabel("") @@ -110,25 +110,15 @@ class MainWindow(QMainWindow): def generate_tab_media_file(self, db_configuration): media_file_tab = QWidget() + table_config = KontorModelConfig(db_configuration, self, "media_file") + model = KontorTableModel(table_config) layout = QVBoxLayout() - filter_layout = QHBoxLayout() - download_checkbox = QCheckBox() - download_checkbox.setText("Download") - download_checkbox.checkStateChanged.connect(self.refresh) - self.filter["download"] = download_checkbox - review_checkbox = QCheckBox() - review_checkbox.setText("Review") - review_checkbox.checkStateChanged.connect(self.refresh) - self.filter["review"] = review_checkbox - filter_layout.addWidget(review_checkbox) - filter_layout.addWidget(download_checkbox) - filter_layout.addStretch() - model = MediaFileTableModel(db_configuration, self) + # model = MediaFileTableModel(db_configuration, self) self.data.append(model) media_file_tab.setLayout(layout) table_view = QTableView() table_view.setModel(model) - layout.addLayout(filter_layout) + layout.addLayout(table_config.get_filter_layout()) layout.addWidget(table_view) return media_file_tab diff --git a/gui/model_config.py b/gui/model_config.py new file mode 100644 index 0000000..1bea8a5 --- /dev/null +++ b/gui/model_config.py @@ -0,0 +1,82 @@ +import mariadb +from PySide6.QtWidgets import QHBoxLayout, QCheckBox + + +class KontorModelConfig: + + def __init__(self, db_config, main_window, table_name: str): + self.header = [] + self.filter = {} + self.main_window = main_window + self._table = table_name + self.db_conn = mariadb.connect( + host=db_config['mariadb']['host'], + port=db_config['mariadb']['port'], + user=db_config['mariadb']['user'], + password=db_config['mariadb']['password'], + database=db_config['mariadb']['database'] + ) + self.get_table_config() + + def get_table_id(self): + cursor = self.db_conn.cursor() + cursor.execute("SELECT id, created_date, last_modified_date FROM meta_data_table WHERE table_name=?", (self._table, )) + rows = cursor.fetchall() + if len(rows) == 1: + return rows[0][0] + return None + + def get_table_config(self): + table_id = self.get_table_id() + cursor = self.db_conn.cursor() + cursor.execute("SELECT id, column_name, column_order FROM meta_data_column WHERE table_id=? AND is_shown is true", (table_id, )) + rows = cursor.fetchall() + self.header.clear() + for (column_id, column_name, column_order) in rows: + self.header.insert(column_order-1, column_name) + print(f"retrieved {len(rows)} columns, set {len(self.header)} headers") + + def get_header(self) -> list: + self.get_table_config() + return self.header + + def get_filter(self) -> str: + filter_rule = "" + # print(self.filter["download"].isChecked()) + if self.filter["download"].isChecked(): + # print(self.filter["download"].isChecked()) + filter_rule = "WHERE should_download is true" + if self.filter["review"].isChecked(): + if len(filter_rule) > 0: + filter_rule += " AND " + else: + filter_rule += "WHERE " + filter_rule += "review is true" + print(f"{filter_rule=}") + return filter_rule + + def get_statement(self) -> str: + filter_rule = self.get_filter() + self.get_table_config() + columns = "" + for index in range(len(self.header)): + if index > 0: + columns += ", " + columns += self.header[index] + statement = f"SELECT {columns} FROM media_file {filter_rule}" + return statement + + def get_filter_layout(self) -> QHBoxLayout: + filter_layout = QHBoxLayout() + download_checkbox = QCheckBox() + download_checkbox.setText("Download") + download_checkbox.checkStateChanged.connect(self.main_window.refresh) + self.filter["download"] = download_checkbox + review_checkbox = QCheckBox() + review_checkbox.setText("Review") + review_checkbox.checkStateChanged.connect(self.main_window.refresh) + self.filter["review"] = review_checkbox + filter_layout.addWidget(review_checkbox) + filter_layout.addWidget(download_checkbox) + filter_layout.addStretch() + return filter_layout diff --git a/gui/media_file_model.py b/gui/table_model.py similarity index 52% rename from gui/media_file_model.py rename to gui/table_model.py index e5af2a4..5fc5dc9 100644 --- a/gui/media_file_model.py +++ b/gui/table_model.py @@ -1,41 +1,23 @@ from datetime import datetime -import mariadb -from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt -from PySide6.QtGui import QColor +from PySide6.QtCore import QAbstractTableModel, QModelIndex +from PySide6.QtGui import Qt + +from model_config import KontorModelConfig -class MediaFileTableModel(QAbstractTableModel): +class KontorTableModel(QAbstractTableModel): - def __init__(self, db_config, main_window): + def __init__(self, model_config: KontorModelConfig): super().__init__() - self.main_window = main_window - self._data = [] - self.mariadb_conn = mariadb.connect( - host=db_config['mariadb']['host'], - port=db_config['mariadb']['port'], - user=db_config['mariadb']['user'], - password=db_config['mariadb']['password'], - database=db_config['mariadb']['database'] - ) - self.refresh() + self._main_window = model_config.main_window + self._config = model_config + self._data = None def refresh(self): data = [] - cursor = self.mariadb_conn.cursor() - filter_rule = "" - print(self.main_window.filter["download"].isChecked()) - if self.main_window.filter["download"].isChecked(): - print(self.main_window.filter["download"].isChecked()) - filter_rule = "WHERE should_download is true" - if self.main_window.filter["review"].isChecked(): - if len(filter_rule) > 0: - filter_rule += " AND " - else: - filter_rule += "WHERE " - filter_rule += "review is true" - print(f"{filter_rule=}") - cursor.execute(f"SELECT id, url, review, should_download, file_name, cloud_link FROM media_file {filter_rule}") + cursor = self._config.db_conn.cursor() + cursor.execute(self._config.get_statement()) rows = cursor.fetchall() print(len(rows)) if len(rows) > 0: @@ -46,8 +28,8 @@ class MediaFileTableModel(QAbstractTableModel): self.endResetModel() else: self._data = None - self.layoutChanged.emit() - self.main_window.statusBar.showMessage(f"{len(rows)} Einträge geladen", 3000) + self.layoutChanged.emit() + self._main_window.statusBar.showMessage(f"{len(rows)} Einträge geladen", 3000) def rowCount(self, parent=QModelIndex()): # The length of the outer list. @@ -57,19 +39,7 @@ class MediaFileTableModel(QAbstractTableModel): def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: - match col: - case 0: - return "ID" - case 1: - return "URL" - case 2: - return "Review" - case 3: - return "Download" - case 4: - return "Filename" - case 5: - return "Cloud Link" + return self._config.header[col] if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: return str(col+1) @@ -84,24 +54,23 @@ class MediaFileTableModel(QAbstractTableModel): return value if isinstance(value, bytes): if value == b'\x01': - return self.main_window.tick + return self._main_window.tick else: - return self.main_window.cross + return self._main_window.cross return value if role == Qt.ItemDataRole.DecorationRole: if isinstance(value, bytes): # print('{}: {}'.format(value, type(value))) if value == b'\x01': - return self.main_window.tick + return self._main_window.tick else: - return self.main_window.cross + return self._main_window.cross def columnCount(self, index=QModelIndex()): # The following takes the first sub-list, and returns # the length (only works if all rows are an equal length) - if self._data is None: - return 5 - return len(self._data[0]) + print(f"Header count: {len(self._config.get_header())}") + return len(self._config.get_header()) def setData(self, index, value, role=Qt.ItemDataRole.EditRole): if role == Qt.ItemDataRole.EditRole: From 78632e0e12a6264058c164be6851b8db886160ed Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 6 Jan 2025 17:09:38 +0000 Subject: [PATCH 08/91] Import kontor-spring into directory springboot --- .gitignore | 8 +- springboot/.gitattributes | 9 + springboot/.gitignore | 32 + springboot/README.md | 3 + springboot/build.gradle | 240 ++++ springboot/frontend/themes/kontor/styles.css | 10 + springboot/frontend/themes/kontor/theme.json | 3 + springboot/gradle.properties | 3 + springboot/gradle/libs.versions.toml | 58 + springboot/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43462 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + springboot/gradlew | 249 ++++ springboot/gradlew.bat | 92 ++ springboot/settings.gradle | 24 + .../src/docs/asciidoc/kontor-spring.adoc | 509 +++++++ .../kontor/comics/views/ArtistViewTest.java | 41 + .../kontor/comics/views/ArtistformTest.java | 63 + .../kontor/comics/views/ComicViewTest.java | 43 + .../comics/views/ComicWorkViewTest.java | 43 + .../kontor/comics/views/IssueViewTest.java | 43 + .../comics/views/PublisherViewTest.java | 41 + .../kontor/comics/views/StoryArcViewTest.java | 43 + .../comics/views/TradePaperbackViewTest.java | 47 + .../kontor/comics/views/VolumeViewTest.java | 50 + .../kontor/comics/views/WorktypeViewTest.java | 49 + .../kontor/tysc/views/CardSetViewTest.java | 43 + .../kontor/tysc/views/CardViewTest.java | 43 + .../tysc/views/FieldPositionViewTest.java | 43 + .../kontor/tysc/views/PlayerViewTest.java | 44 + .../kontor/tysc/views/RoosterViewTest.java | 43 + .../kontor/tysc/views/SportViewTest.java | 43 + .../kontor/tysc/views/TeamViewTest.java | 43 + .../kontor/tysc/views/VendorViewTest.java | 43 + .../resources/application.properties | 30 + .../java/de/thpeetz/kontor/Application.java | 24 + .../thpeetz/kontor/admin/AdminConstants.java | 53 + .../thpeetz/kontor/admin/MailProperties.java | 78 ++ .../kontor/admin/SetupModuleAdmin.java | 336 +++++ .../admin/data/AuthorizationMatrix.java | 35 + .../data/AuthorizationMatrixRepository.java | 13 + .../admin/data/MailAccountRepository.java | 8 + .../kontor/admin/data/MetaDataColumn.java | 37 + .../admin/data/MetaDataColumnRepository.java | 10 + .../kontor/admin/data/MetaDataTable.java | 26 + .../admin/data/MetaDataTableRepository.java | 8 + .../thpeetz/kontor/admin/data/ModuleData.java | 25 + .../admin/data/ModuleDataRepository.java | 15 + .../de/thpeetz/kontor/admin/data/Role.java | 30 + .../kontor/admin/data/RoleRepository.java | 18 + .../de/thpeetz/kontor/admin/data/User.java | 62 + .../kontor/admin/data/UserRepository.java | 16 + .../kontor/admin/services/AdminService.java | 110 ++ .../services/KontorUserDetailsService.java | 125 ++ .../kontor/admin/services/MailService.java | 24 + .../admin/services/MetaDataService.java | 101 ++ .../kontor/admin/services/ModuleService.java | 76 ++ .../kontor/admin/views/AdminLayout.java | 36 + .../kontor/admin/views/AuthorizationForm.java | 112 ++ .../kontor/admin/views/AuthorizationView.java | 114 ++ .../thpeetz/kontor/admin/views/LoginView.java | 51 + .../kontor/admin/views/MetaDataForm.java | 113 ++ .../kontor/admin/views/MetaDataView.java | 112 ++ .../kontor/admin/views/ModuleDataForm.java | 100 ++ .../kontor/admin/views/ModuleDataView.java | 118 ++ .../thpeetz/kontor/admin/views/RoleForm.java | 102 ++ .../thpeetz/kontor/admin/views/RoleView.java | 118 ++ .../thpeetz/kontor/admin/views/UserForm.java | 143 ++ .../kontor/admin/views/UserProfileView.java | 73 + .../thpeetz/kontor/admin/views/UserView.java | 135 ++ .../kontor/bookshelf/BookshelfConstants.java | 53 + .../bookshelf/SetupModuleBookshelf.java | 82 ++ .../kontor/bookshelf/data/Article.java | 27 + .../kontor/bookshelf/data/ArticleAuthor.java | 29 + .../data/ArticleAuthorRepository.java | 12 + .../bookshelf/data/ArticleRepository.java | 16 + .../thpeetz/kontor/bookshelf/data/Author.java | 44 + .../bookshelf/data/AuthorRepository.java | 18 + .../thpeetz/kontor/bookshelf/data/Book.java | 42 + .../kontor/bookshelf/data/BookAuthor.java | 29 + .../bookshelf/data/BookAuthorRepository.java | 12 + .../kontor/bookshelf/data/BookRepository.java | 21 + .../bookshelf/data/BookshelfPublisher.java | 32 + .../data/BookshelfPublisherRepository.java | 18 + .../bookshelf/services/BookshelfService.java | 118 ++ .../kontor/bookshelf/views/ArticleForm.java | 106 ++ .../kontor/bookshelf/views/ArticleView.java | 126 ++ .../kontor/bookshelf/views/AuthorForm.java | 117 ++ .../kontor/bookshelf/views/AuthorView.java | 132 ++ .../kontor/bookshelf/views/BookForm.java | 111 ++ .../kontor/bookshelf/views/BookView.java | 124 ++ .../bookshelf/views/BookshelfLayout.java | 34 + .../views/BookshelfPublisherView.java | 131 ++ .../kontor/bookshelf/views/PublisherForm.java | 108 ++ .../thpeetz/kontor/comics/ComicConstants.java | 93 ++ .../kontor/comics/SetupModuleComics.java | 1198 +++++++++++++++++ .../de/thpeetz/kontor/comics/data/Artist.java | 44 + .../kontor/comics/data/ArtistRepository.java | 17 + .../de/thpeetz/kontor/comics/data/Comic.java | 77 ++ .../kontor/comics/data/ComicRepository.java | 19 + .../thpeetz/kontor/comics/data/ComicWork.java | 48 + .../comics/data/ComicWorkRepository.java | 15 + .../de/thpeetz/kontor/comics/data/Issue.java | 42 + .../kontor/comics/data/IssueRepository.java | 17 + .../thpeetz/kontor/comics/data/Publisher.java | 40 + .../comics/data/PublisherRepository.java | 17 + .../thpeetz/kontor/comics/data/StoryArc.java | 31 + .../comics/data/StoryArcRepository.java | 17 + .../kontor/comics/data/TradePaperback.java | 32 + .../comics/data/TradePaperbackRepository.java | 20 + .../de/thpeetz/kontor/comics/data/Volume.java | 42 + .../kontor/comics/data/VolumeRepository.java | 13 + .../thpeetz/kontor/comics/data/Worktype.java | 44 + .../comics/data/WorktypeRepository.java | 18 + .../kontor/comics/services/ComicService.java | 299 ++++ .../kontor/comics/views/ArtistForm.java | 117 ++ .../kontor/comics/views/ArtistView.java | 129 ++ .../kontor/comics/views/ComicForm.java | 130 ++ .../kontor/comics/views/ComicLayout.java | 43 + .../kontor/comics/views/ComicView.java | 139 ++ .../kontor/comics/views/ComicWorkForm.java | 113 ++ .../kontor/comics/views/ComicWorkView.java | 122 ++ .../kontor/comics/views/IssueForm.java | 113 ++ .../kontor/comics/views/IssueView.java | 130 ++ .../kontor/comics/views/PublisherForm.java | 100 ++ .../kontor/comics/views/PublisherView.java | 130 ++ .../kontor/comics/views/StoryArcForm.java | 108 ++ .../kontor/comics/views/StoryArcView.java | 130 ++ .../comics/views/TradePaperBackForm.java | 109 ++ .../comics/views/TradePaperbackView.java | 129 ++ .../kontor/comics/views/VolumeForm.java | 109 ++ .../kontor/comics/views/VolumeView.java | 130 ++ .../kontor/comics/views/WorktypeForm.java | 121 ++ .../kontor/comics/views/WorktypeView.java | 130 ++ .../kontor/common/data/AbstractEntity.java | 34 + .../common/data/AbstractLinkEntity.java | 55 + .../kontor/common/views/AvatarMenuBar.java | 52 + .../kontor/common/views/KontorLayoutUtil.java | 100 ++ .../kontor/common/views/MainLayout.java | 101 ++ .../thpeetz/kontor/common/views/MainView.java | 18 + .../common/views/SeparateMainLayout.java | 31 + .../thpeetz/kontor/mailclient/data/Mail.java | 20 + .../kontor/mailclient/data/MailAccount.java | 29 + .../kontor/mailclient/views/EmailView.java | 222 +++ .../thpeetz/kontor/media/MediaConstants.java | 35 + .../kontor/media/SetupModuleMedia.java | 39 + .../kontor/media/data/MediaArticle.java | 29 + .../media/data/MediaArticleRepository.java | 13 + .../thpeetz/kontor/media/data/MediaFile.java | 40 + .../media/data/MediaFileRepository.java | 14 + .../thpeetz/kontor/media/data/MediaLink.java | 40 + .../thpeetz/kontor/media/data/MediaVideo.java | 40 + .../media/data/MediaVideoRepository.java | 15 + .../media/services/MediaArticleService.java | 42 + .../media/services/MediaFileService.java | 42 + .../media/services/MediaVideoService.java | 39 + .../kontor/media/views/MediaArticleForm.java | 108 ++ .../kontor/media/views/MediaArticleView.java | 171 +++ .../kontor/media/views/MediaFileForm.java | 113 ++ .../kontor/media/views/MediaFileView.java | 186 +++ .../kontor/media/views/MediaVideoForm.java | 113 ++ .../kontor/media/views/MediaVideoView.java | 178 +++ .../kontor/security/SecurityConfig.java | 59 + .../kontor/security/SecurityService.java | 105 ++ .../thpeetz/kontor/tysc/SetupModuleTysc.java | 519 +++++++ .../de/thpeetz/kontor/tysc/TyscConstants.java | 88 ++ .../de/thpeetz/kontor/tysc/data/Card.java | 49 + .../kontor/tysc/data/CardRepository.java | 17 + .../de/thpeetz/kontor/tysc/data/CardSet.java | 41 + .../kontor/tysc/data/CardSetRepository.java | 18 + .../kontor/tysc/data/FieldPosition.java | 52 + .../tysc/data/FieldPositionRepository.java | 22 + .../de/thpeetz/kontor/tysc/data/Player.java | 49 + .../kontor/tysc/data/PlayerRepository.java | 17 + .../de/thpeetz/kontor/tysc/data/Rooster.java | 51 + .../kontor/tysc/data/RoosterRepository.java | 11 + .../de/thpeetz/kontor/tysc/data/Sport.java | 42 + .../kontor/tysc/data/SportRepository.java | 17 + .../de/thpeetz/kontor/tysc/data/Team.java | 50 + .../kontor/tysc/data/TeamRepository.java | 27 + .../de/thpeetz/kontor/tysc/data/Vendor.java | 38 + .../kontor/tysc/data/VendorRepository.java | 17 + .../kontor/tysc/services/CardService.java | 93 ++ .../kontor/tysc/services/SportService.java | 147 ++ .../thpeetz/kontor/tysc/views/CardForm.java | 110 ++ .../kontor/tysc/views/CardSetForm.java | 103 ++ .../kontor/tysc/views/CardSetView.java | 129 ++ .../thpeetz/kontor/tysc/views/CardView.java | 129 ++ .../thpeetz/kontor/tysc/views/PlayerForm.java | 116 ++ .../thpeetz/kontor/tysc/views/PlayerView.java | 130 ++ .../kontor/tysc/views/PositionForm.java | 115 ++ .../kontor/tysc/views/PositionView.java | 130 ++ .../kontor/tysc/views/RoosterForm.java | 117 ++ .../kontor/tysc/views/RoosterView.java | 121 ++ .../thpeetz/kontor/tysc/views/SportForm.java | 103 ++ .../thpeetz/kontor/tysc/views/SportView.java | 129 ++ .../thpeetz/kontor/tysc/views/TeamForm.java | 115 ++ .../thpeetz/kontor/tysc/views/TeamView.java | 130 ++ .../thpeetz/kontor/tysc/views/TyscLayout.java | 41 + .../thpeetz/kontor/tysc/views/VendorForm.java | 115 ++ .../thpeetz/kontor/tysc/views/VendorView.java | 130 ++ .../META-INF/resources/icons/icon.png | Bin 0 -> 45967 bytes .../META-INF/resources/images/offline.png | Bin 0 -> 9507 bytes .../resources/META-INF/resources/offline.html | 38 + springboot/src/main/resources/application.yml | 67 + springboot/src/main/resources/banner.txt | 8 + .../src/main/resources/logback-spring.xml | 42 + .../de/thpeetz/kontor/ApplicationTests.java | 13 + .../kontor/bookshelf/TestConstants.java | 19 + .../data/ArticleAuthorRepositoryTest.java | 74 + .../bookshelf/data/ArticleAuthorTest.java | 77 ++ .../bookshelf/data/ArticleRepositoryTest.java | 61 + .../kontor/bookshelf/data/ArticleTest.java | 45 + .../bookshelf/data/AuthorRepositoryTest.java | 69 + .../kontor/bookshelf/data/AuthorTest.java | 44 + .../data/BookAuthorRepositoryTest.java | 94 ++ .../kontor/bookshelf/data/BookAuthorTest.java | 82 ++ .../bookshelf/data/BookRepositoryTest.java | 87 ++ .../kontor/bookshelf/data/BookTest.java | 56 + .../BookshelfPublisherRepositoryTest.java | 72 + .../data/BookshelfPublisherTest.java | 45 + .../services/BookshelfServiceTest.java | 102 ++ .../kontor/comics/ComicConstantsTest.java | 14 + .../thpeetz/kontor/comics/TestConstants.java | 67 + .../comics/data/ArtistRepositoryTest.java | 73 + .../kontor/comics/data/ArtistTest.java | 48 + .../comics/data/ComicRepositoryTest.java | 53 + .../thpeetz/kontor/comics/data/ComicTest.java | 77 ++ .../comics/data/ComicWorkRepositoryTest.java | 83 ++ .../kontor/comics/data/ComicWorkTest.java | 24 + .../comics/data/IssueRepositoryTest.java | 45 + .../thpeetz/kontor/comics/data/IssueTest.java | 62 + .../comics/data/PublisherRepositoryTest.java | 68 + .../kontor/comics/data/PublisherTest.java | 45 + .../comics/data/StoryArcRepositoryTest.java | 45 + .../kontor/comics/data/StoryArcTest.java | 61 + .../data/TradePaperbackRepositoryTest.java | 59 + .../comics/data/TradePaperbackTest.java | 59 + .../comics/data/VolumeRepositoryTest.java | 61 + .../kontor/comics/data/VolumeTest.java | 64 + .../comics/data/WorktypeRepositoryTest.java | 46 + .../kontor/comics/data/WorktypeTest.java | 83 ++ .../comics/services/ComicServiceTest.java | 350 +++++ .../thpeetz/kontor/media/TestConstants.java | 5 + .../kontor/media/data/MediaArticleTest.java | 32 + .../kontor/media/data/MediaFileTest.java | 37 + .../kontor/media/data/MediaVideoTest.java | 38 + .../services/MediaArticleServiceTest.java | 50 + .../media/services/MediaFileServiceTest.java | 48 + .../media/services/MediaVideoServiceTest.java | 48 + .../de/thpeetz/kontor/tysc/TestConstants.java | 39 + .../kontor/tysc/data/CardRepositoryTest.java | 35 + .../tysc/data/CardSetRepositoryTest.java | 42 + .../thpeetz/kontor/tysc/data/CardSetTest.java | 22 + .../de/thpeetz/kontor/tysc/data/CardTest.java | 22 + .../data/FieldPositionRepositoryTest.java | 59 + .../kontor/tysc/data/FieldPositionTest.java | 44 + .../tysc/data/PlayerRepositoryTest.java | 31 + .../thpeetz/kontor/tysc/data/PlayerTest.java | 53 + .../tysc/data/RoosterRepositoryTest.java | 29 + .../thpeetz/kontor/tysc/data/RoosterTest.java | 47 + .../kontor/tysc/data/SportRepositoryTest.java | 40 + .../thpeetz/kontor/tysc/data/SportTest.java | 44 + .../kontor/tysc/data/TeamRepositoryTest.java | 37 + .../de/thpeetz/kontor/tysc/data/TeamTest.java | 53 + .../tysc/data/VendorRepositoryTest.java | 41 + .../thpeetz/kontor/tysc/data/VendorTest.java | 44 + .../kontor/tysc/services/CardServiceTest.java | 126 ++ .../tysc/services/SportServiceTest.java | 200 +++ .../src/test/resources/application.properties | 30 + 269 files changed, 19844 insertions(+), 4 deletions(-) create mode 100644 springboot/.gitattributes create mode 100644 springboot/.gitignore create mode 100644 springboot/README.md create mode 100644 springboot/build.gradle create mode 100644 springboot/frontend/themes/kontor/styles.css create mode 100644 springboot/frontend/themes/kontor/theme.json create mode 100644 springboot/gradle.properties create mode 100644 springboot/gradle/libs.versions.toml create mode 100644 springboot/gradle/wrapper/gradle-wrapper.jar create mode 100644 springboot/gradle/wrapper/gradle-wrapper.properties create mode 100755 springboot/gradlew create mode 100644 springboot/gradlew.bat create mode 100644 springboot/settings.gradle create mode 100644 springboot/src/docs/asciidoc/kontor-spring.adoc create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistformTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicWorkViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/IssueViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/PublisherViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/StoryArcViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/TradePaperbackViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/VolumeViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/WorktypeViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardSetViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/FieldPositionViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/PlayerViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/RoosterViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/SportViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/TeamViewTest.java create mode 100644 springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/VendorViewTest.java create mode 100644 springboot/src/integrationTest/resources/application.properties create mode 100644 springboot/src/main/java/de/thpeetz/kontor/Application.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/AdminConstants.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/MailProperties.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/AuthorizationMatrix.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/AuthorizationMatrixRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/MailAccountRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataColumn.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataColumnRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataTable.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataTableRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/ModuleData.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/ModuleDataRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/Role.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/RoleRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/User.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/data/UserRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/services/AdminService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/services/KontorUserDetailsService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/services/MailService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/services/ModuleService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/AdminLayout.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/AuthorizationForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/AuthorizationView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/LoginView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/RoleForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/RoleView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/UserForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/UserProfileView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/admin/views/UserView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/BookshelfConstants.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/SetupModuleBookshelf.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Article.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthor.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Author.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/AuthorRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Book.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthor.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisher.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/services/BookshelfService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfLayout.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfPublisherView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/PublisherForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/ComicConstants.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/SetupModuleComics.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/Artist.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/ArtistRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/Comic.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicWork.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicWorkRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/Issue.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/IssueRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/Publisher.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/PublisherRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/StoryArc.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/StoryArcRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/TradePaperback.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/TradePaperbackRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/Volume.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/VolumeRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/Worktype.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/data/WorktypeRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/services/ComicService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicLayout.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/IssueForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/IssueView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/PublisherForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/PublisherView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/StoryArcForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/StoryArcView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/TradePaperBackForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/TradePaperbackView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/VolumeForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/VolumeView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/WorktypeForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/comics/views/WorktypeView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/common/data/AbstractEntity.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/common/data/AbstractLinkEntity.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/common/views/AvatarMenuBar.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/common/views/KontorLayoutUtil.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/common/views/MainLayout.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/common/views/MainView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/common/views/SeparateMainLayout.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/mailclient/data/Mail.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/mailclient/data/MailAccount.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/mailclient/views/EmailView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/MediaConstants.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/SetupModuleMedia.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/data/MediaArticle.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/data/MediaArticleRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFileRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/data/MediaLink.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/data/MediaVideo.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/data/MediaVideoRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/services/MediaArticleService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/services/MediaVideoService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/views/MediaArticleForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/views/MediaArticleView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/views/MediaVideoForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/views/MediaVideoView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/security/SecurityConfig.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/security/SecurityService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/SetupModuleTysc.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/TyscConstants.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/Card.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardSet.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardSetRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/FieldPosition.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/FieldPositionRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/Player.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/PlayerRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/Rooster.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/RoosterRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/Sport.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/SportRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/Team.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/TeamRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/Vendor.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/data/VendorRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/services/CardService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/services/SportService.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardSetForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardSetView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/PlayerForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/PlayerView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/PositionForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/PositionView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/RoosterForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/RoosterView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/SportForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/SportView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/TeamForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/TeamView.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/TyscLayout.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/VendorForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/tysc/views/VendorView.java create mode 100644 springboot/src/main/resources/META-INF/resources/icons/icon.png create mode 100644 springboot/src/main/resources/META-INF/resources/images/offline.png create mode 100644 springboot/src/main/resources/META-INF/resources/offline.html create mode 100644 springboot/src/main/resources/application.yml create mode 100644 springboot/src/main/resources/banner.txt create mode 100644 springboot/src/main/resources/logback-spring.xml create mode 100644 springboot/src/test/java/de/thpeetz/kontor/ApplicationTests.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/TestConstants.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/bookshelf/services/BookshelfServiceTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/ComicConstantsTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/TestConstants.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/ArtistRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/ArtistTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/IssueRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/IssueTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/PublisherRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/PublisherTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/StoryArcRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/StoryArcTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/VolumeRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/VolumeTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/WorktypeRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/data/WorktypeTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/comics/services/ComicServiceTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/media/TestConstants.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/media/data/MediaArticleTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/media/data/MediaFileTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/media/data/MediaVideoTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/media/services/MediaArticleServiceTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/media/services/MediaFileServiceTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/media/services/MediaVideoServiceTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/TestConstants.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardSetRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardSetTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/FieldPositionRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/FieldPositionTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/PlayerRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/PlayerTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/RoosterRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/RoosterTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/SportRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/SportTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/TeamRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/TeamTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/VendorRepositoryTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/data/VendorTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/services/CardServiceTest.java create mode 100644 springboot/src/test/java/de/thpeetz/kontor/tysc/services/SportServiceTest.java create mode 100644 springboot/src/test/resources/application.properties diff --git a/.gitignore b/.gitignore index 45f246d..575904a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -__pycache__ .idea/ -bonus -icons -icons-shadowless +__pycache__/ +bonus/ +icons/ +icons-shadowless/ diff --git a/springboot/.gitattributes b/springboot/.gitattributes new file mode 100644 index 0000000..097f9f9 --- /dev/null +++ b/springboot/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/springboot/.gitignore b/springboot/.gitignore new file mode 100644 index 0000000..0294d4c --- /dev/null +++ b/springboot/.gitignore @@ -0,0 +1,32 @@ +.gradle/ +.settings/ +build/ +bin/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +.project +.classpath +.vscode/ +.idea/ +*.lock +logs/ +frontend/generated +frontend/index.html +package*.json +tsconfig.json +types.d.ts +node_modules/ +vite.* +kontor*Db +tags* +kontorHSQLDB* +.vs/ +.winget +src/main/resources/application-local.properties +src/main/resources/application-prod.properties +src/main/resources/application-*.yml diff --git a/springboot/README.md b/springboot/README.md new file mode 100644 index 0000000..f24bbcd --- /dev/null +++ b/springboot/README.md @@ -0,0 +1,3 @@ +# kontor-spring + +Kontor Anwendung mit Spring Boot und Vaadin \ No newline at end of file diff --git a/springboot/build.gradle b/springboot/build.gradle new file mode 100644 index 0000000..8892faf --- /dev/null +++ b/springboot/build.gradle @@ -0,0 +1,240 @@ +buildscript { + configurations.classpath { + resolutionStrategy.eachDependency { DependencyResolveDetails details -> + if (details.requested.group == 'com.burgstaller' && details.requested.name == 'okhttp-digest' && details.requested.version == '1.10') { + details.useTarget "io.github.rburgst:${details.requested.name}:1.21" + details.because 'Dependency has moved' + } + } + } + repositories { + mavenCentral() + maven { setUrl("https://maven.vaadin.com/vaadin-prereleases") } + maven { setUrl("https://repo.spring.io/milestone") } + } +} + +plugins { + id 'java' + id 'application' + id 'maven-publish' + id "com.google.cloud.artifactregistry.gradle-plugin" version "2.2.0" + id 'jvm-test-suite' + id 'jacoco' + id 'test-report-aggregation' + id 'jacoco-report-aggregation' + alias(libs.plugins.spring.boot) + alias(libs.plugins.spring.dependencies) + alias(libs.plugins.vaadin) + alias(libs.plugins.lombok) + alias(libs.plugins.asciidoctorPdf) + alias(libs.plugins.asciidoctorConvert) + alias(libs.plugins.asciidoctorGems) +} + +repositories { + mavenCentral() + ruby.gems() + maven { setUrl("https://maven.vaadin.com/vaadin-prereleases") } + maven { setUrl("https://repo.spring.io/milestone") } + maven { setUrl("https://maven.vaadin.com/vaadin-addons") } +} + +sourceCompatibility = '17' + +configurations { + developmentOnly + runtimeClasspath { + extendsFrom developmentOnly + } +} + +dependencies { + implementation 'com.vaadin:vaadin-core' + implementation 'com.vaadin:vaadin-spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + implementation 'org.springframework.security:spring-security-oauth2-jose' + implementation 'org.springframework.security:spring-security-oauth2-resource-server' + implementation 'com.h2database:h2' + implementation libs.hsqldb + runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' + implementation 'com.sun.mail:javax.mail:1.6.2' + implementation 'org.hibernate.orm:hibernate-community-dialects' + testImplementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } + testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'com.vaadin:vaadin-testbench-junit5' + testImplementation 'io.projectreactor:reactor-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + asciidoctorGems libs.rouge + //asciidoctorGems libs.diagram +} + +def pdfFile = layout.buildDirectory.file("docs/asciidocPdf/kontor-spring.pdf") +def pdfArtifact = artifacts.add('archives', pdfFile.get().asFile) { + type 'pdf' + builtBy asciidoctorPdf +} + +publishing { + publications { + maven(MavenPublication) { + groupId = group + '.docs' + artifactId = project.name + artifact pdfArtifact + } + bootJava(MavenPublication) { + artifact tasks.named("bootDistTar") + } + } + repositories { + maven { + name = "gitlabPackageRegistry" + url = uri("https://gitlab.com/api/v4/projects/62010300/packages/maven") + credentials(PasswordCredentials) + } + } +} + +final BUILD_DATE = new Date().format('dd.MM.yyyy').toString() + +asciidoctorPdf { + dependsOn asciidoctorGemsPrepare + + baseDirFollowsSourceFile() + + asciidoctorj { + modules { + diagram.use() + } + requires 'rouge' + attributes 'build-gradle': file('build.gradle'), + 'endpoint-url': 'https://www.thpeetz.de', + 'source-highlighter': 'rouge', + 'imagesdir': './images', + 'toc': 'left', + 'toc-title': 'Inhaltsverzeichnis', + 'revdate': BUILD_DATE, + 'revnumber': { project.version.toString() }, + 'revremark': 'Entwurf', + 'chapter-label': '', + 'icons': 'font', + 'idprefix': 'id_', + 'idseparator': '-', + 'docinfo1': '' + } +} + +build.dependsOn asciidoctorPdf + +dependencyManagement { + imports { + mavenBom libs.vaadin.bom.get().toString() + } +} + +application { + mainClass = 'de.thpeetz.kontor.Application' +} + +bootRun { + args = ["--spring.profiles.active=${project.properties['profile'] ?: 'prod'}"] +} + +vaadin { + productionMode = true +} + +testing { + suites { + configureEach { + useJUnitJupiter() + dependencies { + implementation project() + implementation 'com.vaadin:vaadin-core' + implementation 'com.vaadin:vaadin-spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'com.h2database:h2' + implementation libs.hsqldb + implementation libs.sqlite.jdbc + //runtimeOnly 'com.mysql:mysql-connector-j' + runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' + implementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } + implementation 'org.springframework.security:spring-security-test' + implementation 'com.vaadin:vaadin-testbench-junit5' + implementation 'io.projectreactor:reactor-test' + runtimeOnly 'org.junit.platform:junit-platform-launcher' + } + } + test(JvmTestSuite) { + testType = TestSuiteType.UNIT_TEST + targets { + all { + testTask.configure { + reports { + junitXml { + outputPerTestCase = true // defaults to false + mergeReruns = true // defaults to false + } + } + finalizedBy(jacocoTestReport) + } + } + } + } + integrationTest(JvmTestSuite) { + testType = "view-test" + targets { + all { + testTask.configure { + shouldRunAfter(test) + finalizedBy(jacocoTestReport) + } + } + } + } + } +} + +tasks.named('check') { + dependsOn(testing.suites.integrationTest) + dependsOn(testing.suites.test) + dependsOn tasks.named('testAggregateTestReport', TestReport) + dependsOn tasks.named('integrationTestAggregateTestReport', TestReport) +} + + +jacocoTestReport { + dependsOn test, integrationTest + reports { + xml.required = true + csv.required = false + } +} + +reporting { + reports { + testAggregateTestReport(AggregateTestReport) { + testType = TestSuiteType.UNIT_TEST + } + integrationTestAggregateTestReport(AggregateTestReport) { + testType = "view-test" + } + integrationTestCodeCoverageReport(JacocoCoverageReport) { + testType = "view-test" + } + } +} + +wrapper { + gradleVersion = "8.6" +} diff --git a/springboot/frontend/themes/kontor/styles.css b/springboot/frontend/themes/kontor/styles.css new file mode 100644 index 0000000..843559f --- /dev/null +++ b/springboot/frontend/themes/kontor/styles.css @@ -0,0 +1,10 @@ +@media all and (max-width: 1100px) { + .list-view.editing .toolbar, + .list-view.editing .contact-grid { + display: none; + } +} +a[highlight] { + font-weight: bold; + text-decoration: underline; +} diff --git a/springboot/frontend/themes/kontor/theme.json b/springboot/frontend/themes/kontor/theme.json new file mode 100644 index 0000000..0f7a81f --- /dev/null +++ b/springboot/frontend/themes/kontor/theme.json @@ -0,0 +1,3 @@ +{ + "lumoImports" : [ "typography", "color", "spacing", "badge", "utility" ] +} \ No newline at end of file diff --git a/springboot/gradle.properties b/springboot/gradle.properties new file mode 100644 index 0000000..f305204 --- /dev/null +++ b/springboot/gradle.properties @@ -0,0 +1,3 @@ +description='Kontor with Spring Boot' +version=0.1.0-SNAPSHOT +group=de.thpeetz diff --git a/springboot/gradle/libs.versions.toml b/springboot/gradle/libs.versions.toml new file mode 100644 index 0000000..6ec0bbe --- /dev/null +++ b/springboot/gradle/libs.versions.toml @@ -0,0 +1,58 @@ +[versions] +gradle = "8.6" +args4j = "2.33" +commonscli = "1.5.0" +junit = "5.8.2" +logback = "1.1.2" +mockito = "1.9.5" +picoli = "4.7.0" +slf4j = "1.7.22" +hsqldb = "2.7.1" +sqlite = "3.25.2" +spotbugs = "6.0.7" +asciidoctor = "4.0.2" +rouge = "3.15.0" +#diagram = "2.2.2" +diagram = "2.3.1" +sonarqube = "3.3" +cimtConventions = "1.0.0-SNAPSHOT" +springboot = "3.2.5" +springdependencies = "1.1.4" +vaadin = "24.3.8" +lombok = "8.6" + +[libraries] +args4j = { module = "args4j:args4j", version.ref = "args4j" } +commonscli = { module = "commons-cli:commons-cli", version.ref = "commonscli" } +junit = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } +logbackCore = { module = "ch.qos.logback:logback-core", version.ref = "logback" } +logbackClassic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } +mockito = { module = "org.mockito:mockito-all", version.ref = "mockito" } +picocli = { module = "info.picocli:picocli", version.ref = "picoli" } +slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } +hsqldb = { module = "org.hsqldb:hsqldb", version.ref = "hsqldb" } +sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" } +vaadin-bom = { module = "com.vaadin:vaadin-bom", version.ref = "vaadin" } +asciidoctorGradleJvmGems = { module = "org.asciidoctor:asciidoctor-gradle-jvm-gems", version.ref= "asciidoctor" } +asciidoctorGradleJvm = { module = "org.asciidoctor:asciidoctor-gradle-jvm", version.ref= "asciidoctor" } +asciidoctorGradleJvmPdf = { module = "org.asciidoctor:asciidoctor-gradle-jvm-pdf", version.ref= "asciidoctor" } +rouge = { module = "rubygems:rouge", version.ref = "rouge" } +diagram = { module = "rubygems:asciidoctor-diagram", version.ref = "diagram" } + +[bundles] +logback = ["logbackCore", "logbackClassic"] + +[plugins] +spotbugs = { id = "com.github.spotbugs", version.ref = "spotbugs" } +sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" } +asciidoctorPdf = { id = "org.asciidoctor.jvm.pdf", version.ref = "asciidoctor" } +asciidoctorConvert = { id = "org.asciidoctor.jvm.convert", version.ref = "asciidoctor" } +asciidoctorGems = { id = "org.asciidoctor.jvm.gems", version.ref = "asciidoctor" } +javaConvention = { id = "de.cimt.java-conventions", version.ref = "cimtConventions" } +applicationConvention = { id = "de.cimt.application-conventions", version.ref = "cimtConventions" } +libraryConvention = { id = "de.cimt.library-conventions", version.ref = "cimtConventions" } +asciidoctorConvention = { id = "de.cimt.asciidoctor-conventions", version.ref = "cimtConventions" } +spring-boot = { id = "org.springframework.boot", version.ref = "springboot"} +spring-dependencies = { id = "io.spring.dependency-management", version.ref = "springdependencies" } +vaadin = { id = "com.vaadin", version.ref = "vaadin" } +lombok = { id = "io.freefair.lombok", version.ref = "lombok" } diff --git a/springboot/gradle/wrapper/gradle-wrapper.jar b/springboot/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d64cd4917707c1f8861d8cb53dd15194d4248596 GIT binary patch literal 43462 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 literal 0 HcmV?d00001 diff --git a/springboot/gradle/wrapper/gradle-wrapper.properties b/springboot/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a80b22c --- /dev/null +++ b/springboot/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/springboot/gradlew b/springboot/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/springboot/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/springboot/gradlew.bat b/springboot/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/springboot/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/springboot/settings.gradle b/springboot/settings.gradle new file mode 100644 index 0000000..f559dac --- /dev/null +++ b/springboot/settings.gradle @@ -0,0 +1,24 @@ +pluginManagement { + resolutionStrategy { + eachPlugin { + if (requested.id.id == 'org.springframework.boot') { + useModule("org.springframework.boot:spring-boot-gradle-plugin:${requested.version}") + } + if (requested.id.id == 'org.gradle.toolchains.foojay-resolver') { + useModule("org.gradle.toolchains.foojay-resolver-convention:0.4.0") + } + } + } + repositories { + gradlePluginPortal() + mavenCentral() + maven { setUrl("https://maven.vaadin.com/vaadin-prereleases") } + maven { setUrl("https://repo.spring.io/milestone") } + maven { url 'https://plugins.gradle.org/m2/' } + } +// plugins { +// id 'com.vaadin' version "${vaadinVersion}" +// } +} + +rootProject.name = 'kontor-spring' diff --git a/springboot/src/docs/asciidoc/kontor-spring.adoc b/springboot/src/docs/asciidoc/kontor-spring.adoc new file mode 100644 index 0000000..bd4ee00 --- /dev/null +++ b/springboot/src/docs/asciidoc/kontor-spring.adoc @@ -0,0 +1,509 @@ += Projektbeschreibung kontor-spring: Entwicklungs- und Projekthandbuch +:author: Thomas Peetz +:email: +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: +:toclevels: 4 +:table-caption!: +:counter: table-number: 0 + +[title="Dokumenthistorie", id="Table-{counter:table-number}", options="header"] +|=== +| Version | Datum | Autor | Änderungsgrund / Bemerkungen +| 1.0.0 | 16.05.2022 | Thomas Peetz | Ersterstellung +|=== + +== Allgemeines + +=== Zweck des Dokumentes + +Das Entwicklungshandbuch beschreibt die Werkzeuge und die Vorgehensweise bei der Entwicklung +im Projekt kontor-spring und der Erstellung der Dokumentation. + +=== Verwendete Tools + +==== Gitea + +Für die Verwaltung des Sourcecode kommt ((Gitea))<> zum Einsatz. +Mit Gitea werden auch die Projektaufgaben verwaltet. + +Das Projekt und das dazugehörige Git Repository sind unter der Adresse + +https://gitea.thpeetz.de/kontor/kontor-spring + +zu finden. + +== Erstellung der Dokumentation + +Die Dokumentation des Projektes wird mit ((Asciidoctor))<> geschrieben. +Die Dokumente erhalten ihre Namen nach dem jeweiligen Hauptdokument. + +=== Quellcode Verwaltung + +Die Asciidoctor-Dateien haben die Endung `.adoc`. + +=== Buildsystem + +Zur Erstellung der PDF-Dateien aus den Asciidoctor-Dateien wird das Buildsystem ((Gradle))<> verwendet. +Die Dateien für die Dokumente liegen im Verzeichnis `src/docs/asciidoc`. + +Der Gradle Build wird über die Datei `build.gradle` definiert. + + +== Einführung + +=== Zweck + +=== Stakeholder des Systems + +=== Systemumfang + +==== Zielsetzung des Systems + +=== Systemübersicht + +==== Systemkontext + +==== Systemarchitektur + +==== Systemschnittstellen + +===== Realisierte Schnittstellen + +===== Verwendete Schnittstellen + +==== Logisches Datenmodell + +===== Benutzer ER-Diagramm + +[mermaid, kontor-user-er, png] +.Benutzer ER-Diagramm +.... +erDiagram + user { + string id PK + datetime created_date + datetime last_modified_date + int version + string email + boolean enabled + string firstName + string lastName + string password + string token + boolean tokenExpired + string userName UNIQUE + } + role { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + } + authorization_matrix { + string id PK + datetime created_date + datetime last_modified_date + int version + string user_id FK + string role_id FK + } + module_data { + string id PK + datetime created_date + datetime last_modified_date + int version + boolean import_data + string module_name UNIQUE + } + user ||--o{ authorization_matrix : "matrix" + role ||--o{ authorization_matrix : "matrix" +.... + + +===== Comics ER-Diagramm + +[mermaid, kontor-comics-er, png] +.Comics ER-Diagramm +.... +erDiagram + comic { + string id PK + datetime created_date + datetime last_modified_date + int version + boolean completed + boolean currentOrder + string title + string publisher_id FK + } + volume { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + string comic_id FK + } + issue { + string id PK + datetime created_date + datetime last_modified_date + int version + boolean in_stock + boolean is_read + string issue_number + string comic_id FK + string volume_id FK + } + publisher { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + } + artist { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + } + story_arc { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + string comic_id FK + } + trade_paperback { + string id PK + datetime created_date + datetime last_modified_date + int version + int issueStart + int issueEnd + string name + string comic_id FK + } + worktype { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + } + comic_work { + string id PK + datetime created_date + datetime last_modified_date + int version + string artist_id FK + string comic_id FK + string worktype_id FK + } + comic ||--o{ comic_work : "1" + artist ||--o{ comic_work : "1" + worktype ||--o{ comic-work : "1" + publisher ||--o{ comic : "1" + comic ||--o{ issue : "1" + comic ||--o{ volume : "1" + comic ||--o{ story_arc : "1" + comic ||--o{ trade_paperback : "1" + volume ||--o{ issue : "1" +.... + +===== TYSC ER-Diagramm + +[mermaid, kontor-tysc-er, png] +.TYSC ER-Diagramm +.... +erDiagram + sport { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + } + team { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + string short_name + string sport_id FK + } + field_position { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + string short_name + string sport_id FK + } + rooster { + string id PK + datetime created_date + datetime last_modified_date + int version + int year + string player_id FK + string position_id FK + string team_id FK + } + player { + string id PK + datetime created_date + datetime last_modified_date + int version + string first_name + string last_name + } + vendor { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + } + card_set { + string id PK + datetime created_date + datetime last_modified_date + int version + boolean insert_set + string name + boolean parallel_set + string vendor_id FK + } + card { + string id PK + datetime created_date + datetime last_modified_date + int version + int cardNumber + int year + string card_set FK + string rooster_id FK + string vendor_id FK + } + sport ||--o{ team : "1" + sport ||--o{ field_position : "1" + field_position ||--o{ rooster : "1" + player ||--o{ rooster : "1" + team ||--o{ rooster : "1" + vendor ||--o{ card : "1" + card_set ||--o{ card : "1" + rooster ||--o{ card : "1" +.... + +===== Bookshelf ER-Diagramm + +[mermaid, kontor-bookshelf-er, png] +.Bookshelf ER-Diagramm +.... +erDiagram + article { + string id PK + datetime created_date + datetime last_modified_date + int version + string title + } + book { + string id PK + datetime created_date + datetime last_modified_date + int version + string isbn UNIQUE + string title + int year + string publisher_id FK + } + bookshelf_publisher { + string id PK + datetime created_date + datetime last_modified_date + int version + string name UNIQUE + } + author { + string id PK + datetime created_date + datetime last_modified_date + int version + string first_name + string last_name + } + article_author { + string id PK + datetime created_date + datetime last_modified_date + int version + string article_id FK + string author_id FK + } + book_author { + string id PK + datetime created_date + datetime last_modified_date + int version + string book_id FK + string author_id FK + } + publisher ||--o{ book : "1" + article ||--o{ article_author : "1" + author ||--o{ article_author : "1" + book ||--o{ book_author : "1" + author ||--o{ book_author : "1" +.... + +===== Mail ER-Diagramm + +[mermaid, kontor-mail-er, png] +.Mail ER-Diagramm +.... +erDiagram + mail { + string id PK + datetime created_date + datetime last_modified_date + int version + string subject + string content + datetime received_date + datetime sent_date + } + mail_account { + string id PK + datetime created_date + datetime last_modified_date + int version + string host + string password + int port + string protocol + boolean start_tls + string user_name + } + mail_address { + string id PK + datetime created_date + datetime last_modified_date + int version + string internet_address UNIQUE + string personal + string user_id FK + } + user ||--o{ mail_address : "1" +.... + +==== Einschränkungen + +== Anforderungen der Domäne + +=== Systemfunktionen + +==== Anwendungsfälle + +==== Akteure + +==== Zielgruppen + +=== Anforderungen + +==== Anforderungen an externe Schnittstellen + +==== Funktionale Anforderungen + +==== Qualitätsanforderungen + +==== Randbedingungen + +==== Weitere Anforderungen + +==== Wartungs- und Supportinformationen + +=== Verifikation + +== Projektbeschreibung + +=== Ausgangslage + +//==== Rechtliche Vorgaben und Rahmenbedingungen +//=== Rahmenbedingungen + +//==== Vorhandene Regelungen + +=== Projektziele + +=== Projektabgrenzung + +//=== Voraussichtliche Kosten + +//=== Projektrisiken + +//==== Produktivität + +//==== Finanzielle Risiken + +//==== Akzeptanz + +== Projektorganisation + +=== Projekt-Aufbauorganisation + +=== Rollendefinition + +//==== Projektauftraggeber + +//==== Projektausschuss + +//==== Beratung / Qualitätssicherung + +==== Projekteiter + +==== Projektteam + +==== Liste der Stakeholder + +=== Projektablauforganisation + +==== Projekt-Phasen + +===== Erstellung der Projektdokumentation + + +== Verschiedenes + +=== Erreichbarkeiten + +[bibliography] +== Referenzen + +- [[[asciidoctor]]] http://asciidoctor.org +- [[[gitea]]] http://www.gitea.org +- [[[gradle]]] http://www.gradle.org +- [[[jenkins]]] http://jenkins-ci.org + +[glossary] +== Glossar + +[index] +== Index + +== Verzeichnisse + +=== Abbildungsverzeichnis + +=== Tabellenverzeichnis + +<> <> diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistViewTest.java new file mode 100644 index 0000000..e2eec07 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistViewTest.java @@ -0,0 +1,41 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.Artist; + +@SpringBootTest +class ArtistViewTest { + + @Autowired + private ArtistView artistView; + + @Test + void formShownWhenArtistSelected() { + Grid grid = artistView.getGrid(); + Artist firstArtist = getFirstItem(grid); + + ArtistForm form = artistView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstArtist); + assertTrue(form.isVisible()); + assertEquals(firstArtist.getName(), form.name.getValue()); + } + + private Artist getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List artists = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(5, count); + return artists.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistformTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistformTest.java new file mode 100644 index 0000000..a6aea11 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistformTest.java @@ -0,0 +1,63 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.data.Artist; + +@SpringBootTest +class ArtistformTest { + + private Artist artist1; + private static final String ARTISTNAME= "Lee, Stan"; + + @BeforeEach + void setupData() { + artist1 = new Artist(); + artist1.setName(ARTISTNAME); + } + + @Test + void formFieldsPopulated() { + ArtistForm form = new ArtistForm(); + form.setArtist(artist1); + assertEquals(ARTISTNAME, form.name.getValue()); + } + + @Test + void saveEventHasCorrectValues() { + ArtistForm form = new ArtistForm(); + Artist artist = new Artist(); + form.setArtist(artist); + form.name.setValue(ARTISTNAME); + + AtomicReference savedArtistReference = new AtomicReference<>(null); + form.addSaveListener(e -> { + savedArtistReference.set(e.getArtist()); + }); + form.save.click(); + Artist savedArtist = savedArtistReference.get(); + assertEquals(ARTISTNAME, savedArtist.getName()); + } + + @Test + void deleteEventHasCorrectValues() { + ArtistForm form = new ArtistForm(); + Artist artist = new Artist(); + form.setArtist(artist); + form.name.setValue(ARTISTNAME); + + AtomicReference deletedArtistReference = new AtomicReference<>(null); + form.addDeleteListener(e -> { + deletedArtistReference.set(e.getArtist()); + }); + form.delete.click(); + Artist deletedArtist = deletedArtistReference.get(); + assertEquals(ARTISTNAME, deletedArtist.getName()); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicViewTest.java new file mode 100644 index 0000000..084d64e --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.Comic; + +@SpringBootTest +public class ComicViewTest { + + @Autowired + private ComicView comicView; + + @Test + void formShownWhenComicSelected() { + Grid grid = comicView.getGrid(); + Comic firstComic = getFirstItem(grid); + + ComicForm form = comicView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstComic); + assertTrue(form.isVisible()); + assertEquals(firstComic.getTitle(), form.title.getValue()); + } + + private Comic getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List comics = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(169, count); + return comics.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicWorkViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicWorkViewTest.java new file mode 100644 index 0000000..0d3113e --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicWorkViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.ComicWork; + +@SpringBootTest +class ComicWorkViewTest { + + @Autowired + private ComicWorkView comicWorkView; + + @Test + void formShownWhenComicSelected() { + Grid grid = comicWorkView.getGrid(); + ComicWork firstComicWork = getFirstItem(grid); + + ComicWorkForm form = comicWorkView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstComicWork); + assertTrue(form.isVisible()); + assertEquals(firstComicWork.getComic(), form.comic.getValue()); + } + + private ComicWork getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List comicWorks = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(18, count); + return comicWorks.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/IssueViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/IssueViewTest.java new file mode 100644 index 0000000..089f8f5 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/IssueViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.Issue; + +@SpringBootTest +public class IssueViewTest { + + @Autowired + private IssueView issueView; + + @Test + void formShownWhenIssueSelected() { + Grid grid = issueView.getGrid(); + Issue firstIssue = getFirstItem(grid); + + IssueForm form = issueView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstIssue); + assertTrue(form.isVisible()); + assertEquals(firstIssue.getIssueNumber(), form.issueNumber.getValue()); + } + + private Issue getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List issues = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(750, count); + return issues.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/PublisherViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/PublisherViewTest.java new file mode 100644 index 0000000..b3bab80 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/PublisherViewTest.java @@ -0,0 +1,41 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.Publisher; + +@SpringBootTest +class PublisherViewTest { + + @Autowired + private PublisherView publisherView; + + @Test + void formShownWhenPublisherSelected() { + Grid grid = publisherView.getGrid(); + Publisher firstPublisher = getFirstItem(grid); + + PublisherForm form = publisherView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstPublisher); + assertTrue(form.isVisible()); + assertEquals(firstPublisher.getName(), form.name.getValue()); + } + + private Publisher getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List publishers = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(18, count); + return publishers.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/StoryArcViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/StoryArcViewTest.java new file mode 100644 index 0000000..56f9c34 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/StoryArcViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.StoryArc; + +@SpringBootTest +class StoryArcViewTest { + + @Autowired + private StoryArcView storyArcView; + + @Test + void formShownWhenStoryArcSelected() { + Grid grid = storyArcView.getGrid(); + StoryArc firstStoryArc = getFirstItem(grid); + + StoryArcForm form = storyArcView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstStoryArc); + assertTrue(form.isVisible()); + assertEquals(firstStoryArc.getName(), form.name.getValue()); + } + + private StoryArc getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List storyArcs = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(3, count); + return storyArcs.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/TradePaperbackViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/TradePaperbackViewTest.java new file mode 100644 index 0000000..a3b4119 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/TradePaperbackViewTest.java @@ -0,0 +1,47 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.TradePaperback; +import de.thpeetz.kontor.comics.data.Volume; + +@SpringBootTest +class TradePaperbackViewTest { + + @Autowired + private TradePaperbackView tradePaperbackView; + + @Test + void formShownWhenVolumeSelected() { + Grid grid = tradePaperbackView.getGrid(); + + TradePaperback firstTradePaperback = getFirstItem(grid); + + TradePaperBackForm form = tradePaperbackView.getForm(); + assertFalse(form.isVisible()); + + if (firstTradePaperback != null) { + grid.asSingleSelect().setValue(firstTradePaperback); + assertTrue(form.isVisible()); + assertEquals(firstTradePaperback.getName(), form.name.getValue()); + } + } + + private TradePaperback getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List tradePaperbacks = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(40, count); + return tradePaperbacks.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/VolumeViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/VolumeViewTest.java new file mode 100644 index 0000000..15e8d15 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/VolumeViewTest.java @@ -0,0 +1,50 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.Volume; + +@SpringBootTest +class VolumeViewTest { + + @Autowired + private VolumeView volumeView; + + @Test + void formShownWhenVolumeSelected() { + Grid grid = volumeView.getGrid(); + + Volume firstVolume = getFirstItem(grid); + + VolumeForm form = volumeView.getForm(); + assertFalse(form.isVisible()); + + if (firstVolume != null) { + grid.asSingleSelect().setValue(firstVolume); + assertTrue(form.isVisible()); + assertEquals(firstVolume.getName(), form.name.getValue()); + } + } + + private Volume getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List volumes = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(0, count); + if (count > 0) { + return volumes.get(0); + } else { + return null; + } + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/WorktypeViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/WorktypeViewTest.java new file mode 100644 index 0000000..2f15672 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/comics/views/WorktypeViewTest.java @@ -0,0 +1,49 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.Worktype; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringBootTest +class WorktypeViewTest { + + @Autowired + private WorktypeView worktypeView; + + @Test + void formShownWhenWorktypeSelected() { + Grid grid = worktypeView.getGrid(); + + Worktype firstWorktype = getFirstItem(grid); + + WorktypeForm form = worktypeView.getForm(); + assertFalse(form.isVisible()); + + if (firstWorktype != null) { + grid.asSingleSelect().setValue(firstWorktype); + assertTrue(form.isVisible()); + assertEquals(firstWorktype.getName(), form.name.getValue()); + } + } + + private Worktype getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List worktypes = grid.getListDataView().getItems().collect(Collectors.toList()); + log.info("found worktypes: {}", worktypes); + assertEquals(3, count); + return worktypes.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardSetViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardSetViewTest.java new file mode 100644 index 0000000..40d3fbb --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardSetViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.CardSet; + +@SpringBootTest +class CardSetViewTest { + + @Autowired + private CardSetView cardSetView; + + @Test + void formShownWhenCardSetSelected() { + Grid grid = cardSetView.getGrid(); + CardSet firstCardSet = getFirstItem(grid); + + CardSetForm form = cardSetView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstCardSet); + assertTrue(form.isVisible()); + assertEquals(firstCardSet.getName(), form.name.getValue()); + } + + private CardSet getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List cardSets = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(15, count); + return cardSets.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardViewTest.java new file mode 100644 index 0000000..0a5a3ed --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.Card; + +@SpringBootTest +class CardViewTest { + + @Autowired + private CardView cardView; + + @Test + void formShownWhenCardSelected() { + Grid grid = cardView.getGrid(); + Card firstCard = getFirstItem(grid); + + CardForm form = cardView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstCard); + assertTrue(form.isVisible()); + assertEquals(String.valueOf(firstCard.getCardNumber()), form.cardNumber.getValue()); + } + + private Card getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List cards = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(10, count); + return cards.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/FieldPositionViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/FieldPositionViewTest.java new file mode 100644 index 0000000..737b74d --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/FieldPositionViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.FieldPosition; + +@SpringBootTest +class FieldPositionViewTest { + + @Autowired + private PositionView positionView; + + @Test + void formShownWhenPositionSelected() { + Grid grid = positionView.getGrid(); + FieldPosition firstFieldPosition = getFirstItem(grid); + + PositionForm form = positionView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstFieldPosition); + assertTrue(form.isVisible()); + assertEquals(firstFieldPosition.getName(), form.name.getValue()); + } + + private FieldPosition getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List positions = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(44, count); + return positions.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/PlayerViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/PlayerViewTest.java new file mode 100644 index 0000000..5e7d640 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/PlayerViewTest.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.Player; + +@SpringBootTest +class PlayerViewTest { + + @Autowired + private PlayerView playerView; + + @Test + void formShownWhenPlayerSelected() { + Grid grid = playerView.getGrid(); + Player firstPlayer = getFirstItem(grid); + + PlayerForm form = playerView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstPlayer); + assertTrue(form.isVisible()); + assertEquals(firstPlayer.getLastName(), form.lastName.getValue()); + assertEquals(firstPlayer.getFirstName(), form.firstName.getValue()); + } + + private Player getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List players = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(38, count); + return players.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/RoosterViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/RoosterViewTest.java new file mode 100644 index 0000000..d04f306 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/RoosterViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.Rooster; + +@SpringBootTest +class RoosterViewTest { + + @Autowired + private RoosterView roosterView; + + @Test + void formShownWhenRoosterSelected() { + Grid grid = roosterView.getGrid(); + Rooster firstRooster = getFirstItem(grid); + + RoosterForm form = roosterView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstRooster); + assertTrue(form.isVisible()); + assertEquals(firstRooster.getYear(), form.year.getValue()); + } + + private Rooster getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List roosters = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(11, count); + return roosters.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/SportViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/SportViewTest.java new file mode 100644 index 0000000..ca86d0a --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/SportViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.Sport; + +@SpringBootTest +class SportViewTest { + + @Autowired + private SportView sportView; + + @Test + void formShownWhenSportSelected() { + Grid grid = sportView.getGrid(); + Sport firstSport = getFirstItem(grid); + + SportForm form = sportView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstSport); + assertTrue(form.isVisible()); + assertEquals(firstSport.getName(), form.name.getValue()); + } + + private Sport getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List sports = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(4, count); + return sports.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/TeamViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/TeamViewTest.java new file mode 100644 index 0000000..da0ea19 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/TeamViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.Team; + +@SpringBootTest +class TeamViewTest { + + @Autowired + private TeamView teamView; + + @Test + void formShownWhenTeamSelected() { + Grid grid = teamView.getGrid(); + Team firstTeam = getFirstItem(grid); + + TeamForm form = teamView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstTeam); + assertTrue(form.isVisible()); + assertEquals(firstTeam.getName(), form.name.getValue()); + } + + private Team getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List teams = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(122, count); + return teams.get(0); + } +} diff --git a/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/VendorViewTest.java b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/VendorViewTest.java new file mode 100644 index 0000000..ef80d22 --- /dev/null +++ b/springboot/src/integrationTest/java/de/thpeetz/kontor/tysc/views/VendorViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.Vendor; + +@SpringBootTest +class VendorViewTest { + + @Autowired + private VendorView vendorView; + + @Test + void formShownWhenVendorSelected() { + Grid grid = vendorView.getGrid(); + Vendor firstVendor = getFirstItem(grid); + + VendorForm form = vendorView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstVendor); + assertTrue(form.isVisible()); + assertEquals(firstVendor.getName(), form.name.getValue()); + } + + private Vendor getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List vendors = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(9, count); + return vendors.get(0); + } +} diff --git a/springboot/src/integrationTest/resources/application.properties b/springboot/src/integrationTest/resources/application.properties new file mode 100644 index 0000000..27a1935 --- /dev/null +++ b/springboot/src/integrationTest/resources/application.properties @@ -0,0 +1,30 @@ +server.port=8085 + +spring.hibernate.dialect=org.hibernate.dialect.HSQLDialect +spring.jpa.database-platform=org.hibernate.dialect.HSQLDialect +spring.datasource.driverClassName=org.hsqldb.jdbc.JDBCDriver +spring.datasource.url=jdbc:hsqldb:mem:itDb +spring.datasource.username=sa +spring.datasource.password=sa + +#spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect +#spring.datasource.driverClassName=org.sqlite.JDBC +#spring.datasource.url=jdbc:sqlite:file:./kontorITDb?cache=shared +#spring.datasource.username=sa +#spring.datasource.password=sa + +spring.jpa.defer-datasource-initialization = true +#spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=false +spring.sql.init.mode=always + +spring.mustache.check-template-location = false + +logging.level.org.atmosphere=INFO +logging.level.org.springframework.web=INFO +logging.level.guru.springframework.controllers=DEBUG +logging.level.org.hibernate=INFO +logging.level.de.thpeetz=DEBUG + +jwt.auth.secret=J6GOtcwC2NJI1l0VkHu20PacPFGTxpirBxWwynoHjsc= diff --git a/springboot/src/main/java/de/thpeetz/kontor/Application.java b/springboot/src/main/java/de/thpeetz/kontor/Application.java new file mode 100644 index 0000000..c304f35 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/Application.java @@ -0,0 +1,24 @@ +package de.thpeetz.kontor; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import com.vaadin.flow.component.page.AppShellConfigurator; +import com.vaadin.flow.server.PWA; +import com.vaadin.flow.theme.Theme; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Slf4j +@EnableJpaAuditing +@SpringBootApplication +@Theme(value = "kontor") +@PWA(name = "Vaadin Kontor", shortName = "Kontor", offlinePath = "offline.html", offlineResources = { "images/offline.png" }) +public class Application implements AppShellConfigurator { + + public static void main(String[] args) { + log.info("Starting Kontor application"); + SpringApplication.run(Application.class); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/AdminConstants.java b/springboot/src/main/java/de/thpeetz/kontor/admin/AdminConstants.java new file mode 100644 index 0000000..63f11b1 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/AdminConstants.java @@ -0,0 +1,53 @@ +package de.thpeetz.kontor.admin; + +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.RouterLink; +import de.thpeetz.kontor.admin.views.*; +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.views.ComicWorkView; + +public class AdminConstants { + + + private AdminConstants() { + // private constructor to hide the implicit public one + } + + public static final String ADMIN_TITLE = "Verwaltung"; + public static final String AUTHORIZATION = "Berechtigungen"; + public static final String AUTHORIZATION_ROUTE = "/admin/authorization"; + public static final String DATA = "Daten"; + public static final String METADATA_ROUTE = "admin/metadata"; + public static final String ROLE = "Rollen"; + public static final String ROLE_ROUTE = "/admin/role"; + public static final String USER = "Benutzer"; + public static final String USER_ROUTE = "/admin/user"; + public static final String ADMIN = "admin"; + public static final String ADMIN_ROUTE = "/admin"; + + public static RouterLink getUserNavigation() { + return new RouterLink(USER, UserView.class); + } + + public static RouterLink getRoleNavigation() { + return new RouterLink(ROLE, RoleView.class); + } + + public static RouterLink getAuthorizationNavigation() { + return new RouterLink(AUTHORIZATION, AuthorizationView.class); + } + + public static SideNavItem getAdminNavigation() { + SideNavItem administration = new SideNavItem(ADMIN_TITLE, USER_ROUTE, VaadinIcon.GROUP.create()); + administration.addItem(new SideNavItem(USER, USER_ROUTE, VaadinIcon.USERS.create())); + administration.addItem(new SideNavItem(ROLE, RoleView.class)); + SideNavItem data = new SideNavItem(DATA, AUTHORIZATION_ROUTE, VaadinIcon.DATABASE.create()); + data.addItem(new SideNavItem(ComicConstants.COMICWORK, ComicWorkView.class)); + data.addItem(new SideNavItem(AUTHORIZATION, AuthorizationView.class)); + data.addItem(new SideNavItem("Data Import", ModuleDataView.class)); + data.addItem(new SideNavItem("Meta Data", MetaDataView.class)); + administration.addItem(data); + return administration; + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/MailProperties.java b/springboot/src/main/java/de/thpeetz/kontor/admin/MailProperties.java new file mode 100644 index 0000000..9e8a6b4 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/MailProperties.java @@ -0,0 +1,78 @@ +package de.thpeetz.kontor.admin; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "mail") +public class MailProperties { + + private String protocol; + private String host; + private Integer port; + private String userName; + private String password; + private Boolean startTls; + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public Boolean getStartTls() { + return startTls; + } + + public void setStartTls(Boolean startTls) { + this.startTls = startTls; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("MailProperties{"); + sb.append("protocol='").append(protocol).append('\''); + sb.append(", host='").append(host).append('\''); + sb.append(", port=").append(port); + sb.append(", starttls=").append(startTls); + sb.append(", userName='").append(userName).append('\''); + sb.append(", password='").append(password).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java new file mode 100644 index 0000000..419c519 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java @@ -0,0 +1,336 @@ +package de.thpeetz.kontor.admin; + +import de.thpeetz.kontor.admin.data.*; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.admin.services.MetaDataService; +import de.thpeetz.kontor.mailclient.data.MailAccount; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.List; + +@Slf4j +@Component +public class SetupModuleAdmin implements ApplicationListener { + boolean alreadySetup = false; + + @Autowired + private UserRepository userRepository; + + @Autowired + private AuthorizationMatrixRepository authorizationMatrixRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private MailAccountRepository mailAccountRepository; + + @Autowired + private MailProperties mailProperties; + + @Autowired + private AdminService adminService; + + @Autowired + private MetaDataService metaDataService; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + if (alreadySetup) { + return; + } + + // Create initial roles and users + Role adminRole = adminService.addRole("ROLE_ADMIN"); + Role userRole = adminService.addRole("ROLE_USER"); + List users = userRepository.findAll(); + if (users.isEmpty()) { + User adminUser = initAdminUser(); + initMatrix(adminRole, adminUser); + initMatrix(userRole, adminUser); + } + log.info("MailProperties: {}", mailProperties); + initMail(mailProperties); + + initMetaData(); + } + + private void initMail(MailProperties mailProperties) { + log.info("initMail: Host {} with User {}", mailProperties.getHost(), mailProperties.getUserName()); + if (mailProperties.getHost() == null || mailProperties.getHost().isEmpty()) { + return; + } + boolean addMailAccount = false; + List mailAccounts = mailAccountRepository.findAll(); + if (mailAccounts.isEmpty()) { + addMailAccount = true; + } + for (MailAccount mailAccount : mailAccounts) { + String accountUser = mailAccount.getUserName(); + String propertyUser = mailProperties.getUserName(); + String accountHost = mailAccount.getHost(); + String propertyHost = mailProperties.getHost(); + if (propertyHost.equals(accountHost) && propertyUser.equals(accountUser)) { + log.debug("already configured: {}", mailAccount); + } else { + addMailAccount = true; + } + } + if (addMailAccount) { + log.info("add Mail Account: {}", mailProperties); + MailAccount mailAccount = new MailAccount(); + mailAccount.setProtocol(mailProperties.getProtocol()); + mailAccount.setHost(mailProperties.getHost()); + mailAccount.setPort(mailProperties.getPort()); + mailAccount.setUserName(mailProperties.getUserName()); + mailAccount.setPassword(mailProperties.getPassword()); + mailAccount.setStartTls(mailProperties.getStartTls()); + mailAccountRepository.save(mailAccount); + } + } + + private void initMatrix(Role role, User user) { + log.info("initMatrix: Role {} for User {}", role.getName(), user.getUserName()); + Collection configuredRoles = authorizationMatrixRepository.findByUser(user); + if (configuredRoles.stream().anyMatch(matrix -> matrix.getRole().getId().equals(role.getId()))) { + log.info("Role {} already defined", role.getName()); + } else { + log.info("add Role {} to User {}", role.getName(), user.getUserName()); + final AuthorizationMatrix adminMatrix = new AuthorizationMatrix(); + adminMatrix.setUser(user); + adminMatrix.setRole(role); + authorizationMatrixRepository.save(adminMatrix); + } + } + + private User initAdminUser() { + log.info("initAdminUser"); + User admin = userRepository.findByUserName("admin"); + if (admin == null) { + log.info("User admin not found, will be created."); + admin = new User(); + admin.setFirstName("Admin"); + admin.setLastName("Administrator"); + admin.setUserName("admin"); + admin.setEmail("admin@example.org"); + admin.setPassword(passwordEncoder.encode("admin")); + userRepository.save(admin); + } + return admin; + } + + private void initMetaData() { + log.info("initMetaData"); + MetaDataTable mediaArticleTable = metaDataService.getTable("media_article"); + metaDataService.getColumn(mediaArticleTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(mediaArticleTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(mediaArticleTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(mediaArticleTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(mediaArticleTable, "url", "link_url", "TEXT", "UNIQUE", 5); + metaDataService.getColumn(mediaArticleTable, "review", "review", "BOOLEAN", null, 6); + metaDataService.getColumn(mediaArticleTable, "title", "title", "TEXT", null, 7); + MetaDataTable mediaVideoTable = metaDataService.getTable("media_video"); + metaDataService.getColumn(mediaVideoTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(mediaVideoTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(mediaVideoTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(mediaVideoTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(mediaVideoTable, "url", "link_url", "TEXT", "UNIQUE", 5); + metaDataService.getColumn(mediaVideoTable, "review", "review", "BOOLEAN", null, 6); + metaDataService.getColumn(mediaVideoTable, "should_download", "should_download", "BOOLEAN", null, 7); + metaDataService.getColumn(mediaVideoTable, "title", "title", "TEXT", null, 8); + metaDataService.getColumn(mediaVideoTable, "file_name", "file_name", "TEXT", null, 9); + metaDataService.getColumn(mediaVideoTable, "path", "path", "TEXT", null, 10); + metaDataService.getColumn(mediaVideoTable, "cloud_link", "cloud_link", "TEXT", null, 11); + MetaDataTable mediaFileTable = metaDataService.getTable("media_file"); + metaDataService.getColumn(mediaFileTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(mediaFileTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(mediaFileTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(mediaFileTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(mediaFileTable, "url", "link_url", "TEXT", "UNIQUE", 5); + metaDataService.getColumn(mediaFileTable, "review", "review", "BOOLEAN", null, 6); + metaDataService.getColumn(mediaFileTable, "should_download", "should_download", "BOOLEAN", null, 7); + metaDataService.getColumn(mediaFileTable, "title", "title", "TEXT", null, 8); + metaDataService.getColumn(mediaFileTable, "file_name", "file_name", "TEXT", null, 9); + metaDataService.getColumn(mediaFileTable, "path", "path", "TEXT", null, 10); + metaDataService.getColumn(mediaFileTable, "cloud_link", "cloud_link", "TEXT", null, 11); + MetaDataTable artistTable = metaDataService.getTable("artist"); + metaDataService.getColumn(artistTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(artistTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(artistTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(artistTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(artistTable, "name", "name", "TEXT", "UNIQUE", 5); + MetaDataTable publisherTable = metaDataService.getTable("publisher"); + metaDataService.getColumn(publisherTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(publisherTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(publisherTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(publisherTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(publisherTable, "name", "name", "TEXT", "UNIQUE", 5); + MetaDataTable comicTable = metaDataService.getTable("comic"); + metaDataService.getColumn(comicTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(comicTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(comicTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(comicTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(comicTable, "completed", "completed", "BOOLEAN", null, 5); + metaDataService.getColumn(comicTable, "current_order", "current_order", "BOOLEAN", null, 6); + metaDataService.getColumn(comicTable, "title", "title", "TEXT", "UNIQUE", 7); + metaDataService.getColumn(comicTable, "publisher_id", "publisher_id", "TEXT", null, 8); + MetaDataTable issueTable = metaDataService.getTable("issue"); + metaDataService.getColumn(issueTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(issueTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(issueTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(issueTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(issueTable, "in_stock", "in_stock", "BOOLEAN", null, 5); + metaDataService.getColumn(issueTable, "is_read", "is_read", "BOOLEAN", null, 6); + metaDataService.getColumn(issueTable, "issue_number", "issue_number", "TEXT", null, 7); + metaDataService.getColumn(issueTable, "comic_id", "comic_id", "TEXT", null, 8); + metaDataService.getColumn(issueTable, "volume_id", "volume_id", "TEXT", null, 9); + MetaDataTable volumeTable = metaDataService.getTable("volume"); + metaDataService.getColumn(volumeTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(volumeTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(volumeTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(volumeTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(volumeTable, "name", "name", "TEXT", null, 5); + metaDataService.getColumn(volumeTable, "comic_id", "comic_id", "TEXT", null, 6); + MetaDataTable tpbTable = metaDataService.getTable("trade_paperback"); + metaDataService.getColumn(tpbTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(tpbTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(tpbTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(tpbTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(tpbTable, "issue_start", "issue_start", "LONG", null, 5); + metaDataService.getColumn(tpbTable, "issue_end", "issue_end", "LONG", null, 6); + metaDataService.getColumn(tpbTable, "name", "name", "TEXT", null, 7); + metaDataService.getColumn(tpbTable, "comic_id", "comic_id", "TEXT", null, 8); + MetaDataTable storyArcTable = metaDataService.getTable("story_arc"); + metaDataService.getColumn(storyArcTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(storyArcTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(storyArcTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(storyArcTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(storyArcTable, "name", "name", "TEXT", null, 5); + metaDataService.getColumn(storyArcTable, "comic_id", "comic_id", "TEXT", null, 6); + MetaDataTable worktypeTable = metaDataService.getTable("worktype"); + metaDataService.getColumn(worktypeTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(worktypeTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(worktypeTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(worktypeTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(worktypeTable, "name", "name", "TEXT", "UNIQUE", 5); + MetaDataTable comicworkTable = metaDataService.getTable("comic_work"); + metaDataService.getColumn(comicworkTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(comicworkTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(comicworkTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(comicworkTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(comicworkTable, "artist_id", "artist_id", "TEXT", null, 5); + metaDataService.getColumn(comicworkTable, "comic_id", "comic_id", "TEXT", null, 6); + metaDataService.getColumn(comicworkTable, "work_type_id", "work_type_id", "TEXT", null, 7); + MetaDataTable authorTable = metaDataService.getTable("author"); + metaDataService.getColumn(authorTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(authorTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(authorTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(authorTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(authorTable, "first_name", "first_name", "TEXT", null, 5); + metaDataService.getColumn(authorTable, "last_name", "last_name", "TEXT", null, 6); + MetaDataTable articleTable = metaDataService.getTable("article"); + metaDataService.getColumn(articleTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(articleTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(articleTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(articleTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(articleTable, "title", "title", "TEXT", "UNIQUE", 5); + MetaDataTable articleAuthorTable = metaDataService.getTable("article_author"); + metaDataService.getColumn(articleAuthorTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(articleAuthorTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(articleAuthorTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(articleAuthorTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(articleAuthorTable, "article_id", "article_id", "TEXT", null, 5); + metaDataService.getColumn(articleAuthorTable, "author_id", "author_id", "TEXT", null, 6); + MetaDataTable bookTable = metaDataService.getTable("book"); + metaDataService.getColumn(bookTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(bookTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(bookTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(bookTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(bookTable, "isbn", "isbn", "TEXT", "UNIQUE", 5); + metaDataService.getColumn(bookTable, "title", "title", "TEXT", null, 6); + metaDataService.getColumn(bookTable, "year", "year", "LONG", null, 7); + metaDataService.getColumn(bookTable, "publisher_id", "publisher_id", "TEXT", null, 8); + MetaDataTable bookAuthorTable = metaDataService.getTable("book_author"); + metaDataService.getColumn(bookAuthorTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(bookAuthorTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(bookAuthorTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(bookAuthorTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(bookAuthorTable, "author_id", "author_id", "TEXT", null, 5); + metaDataService.getColumn(bookAuthorTable, "book_id", "book_id", "TEXT", null, 6); + MetaDataTable bookshelfPublisherTable = metaDataService.getTable("bookshelf_publisher"); + metaDataService.getColumn(bookshelfPublisherTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(bookshelfPublisherTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(bookshelfPublisherTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(bookshelfPublisherTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(bookshelfPublisherTable, "name", "name", "TEXT", "UNIQUE", 5); + MetaDataTable sportTable = metaDataService.getTable("sport"); + metaDataService.getColumn(sportTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(sportTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(sportTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(sportTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(sportTable, "name", "name", "TEXT", "UNIQUE", 5); + MetaDataTable playerTable = metaDataService.getTable("player"); + metaDataService.getColumn(playerTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(playerTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(playerTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(playerTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(playerTable, "first_name", "first_name", "TEXT", null, 5); + metaDataService.getColumn(playerTable, "last_name", "last_name", "TEXT", null, 6); + MetaDataTable teamTable = metaDataService.getTable("team"); + metaDataService.getColumn(teamTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(teamTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(teamTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(teamTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(teamTable, "name", "name", "TEXT", "UNIQUE", 5); + metaDataService.getColumn(teamTable, "short_name", "short_name", "TEXT", null, 6); + metaDataService.getColumn(teamTable, "sport_id", "sport_id", "TEXT", null, 7); + MetaDataTable vendorTable = metaDataService.getTable("vendor"); + metaDataService.getColumn(vendorTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(vendorTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(vendorTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(vendorTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(vendorTable, "name", "name", "TEXT", "UNIQUE", 5); + MetaDataTable fieldPositionTable = metaDataService.getTable("field_position"); + metaDataService.getColumn(fieldPositionTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(fieldPositionTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(fieldPositionTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(fieldPositionTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(fieldPositionTable, "name", "name", "TEXT", null, 5); + metaDataService.getColumn(fieldPositionTable, "short_name", "short_name", "TEXT", null, 6); + metaDataService.getColumn(fieldPositionTable, "sport_id", "sport_id", "TEXT", null, 7); + MetaDataTable roosterTable = metaDataService.getTable("rooster"); + metaDataService.getColumn(roosterTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(roosterTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(roosterTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(roosterTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(roosterTable, "year", "year", "LONG", null, 5); + metaDataService.getColumn(roosterTable, "player_id", "player_id", "TEXT", null, 6); + metaDataService.getColumn(roosterTable, "position_id", "position_id", "TEXT", null, 7); + metaDataService.getColumn(roosterTable, "team_id", "team_id", "TEXT", null, 8); + MetaDataTable cardSetTable = metaDataService.getTable("card_set"); + metaDataService.getColumn(cardSetTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(cardSetTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(cardSetTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(cardSetTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(cardSetTable, "insert_set", "insert_set", "BOOLEAN", null, 5); + metaDataService.getColumn(cardSetTable, "parallel_set", "parallel_set", "BOOLEAN", null, 6); + metaDataService.getColumn(cardSetTable, "name", "name", "TEXT", null, 7); + metaDataService.getColumn(cardSetTable, "vendor_id", "vendor_id", "TEXT", null, 8); + MetaDataTable cardTable = metaDataService.getTable("card"); + metaDataService.getColumn(cardTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1); + metaDataService.getColumn(cardTable, "created_date", "created", "TIMESTAMP", null, 2); + metaDataService.getColumn(cardTable, "last_modified_date", "modified", "TIMESTAMP", null, 3); + metaDataService.getColumn(cardTable, "version", "version", "LONG", null, 4); + metaDataService.getColumn(cardTable, "card_number", "card_number", "LONG", null, 5); + metaDataService.getColumn(cardTable, "year", "year", "LONG", null, 6); + metaDataService.getColumn(cardTable, "card_set_id", "card_set_id", "TEXT", null, 7); + metaDataService.getColumn(cardTable, "rooster_id", "rooster_id", "TEXT", null, 8); + metaDataService.getColumn(cardTable, "vendor_id", "vendor_id", "TEXT", null, 9); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/AuthorizationMatrix.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/AuthorizationMatrix.java new file mode 100644 index 0000000..26b5ebb --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/AuthorizationMatrix.java @@ -0,0 +1,35 @@ +package de.thpeetz.kontor.admin.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@Entity +public class AuthorizationMatrix extends AbstractEntity { + + @ManyToOne + @JoinColumn(name = "user_id") + @NotNull + private User user; + + @ManyToOne + @JoinColumn(name = "role_id") + @NotNull + private Role role; + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("AuthorizationMatrix{"); + sb.append("user=").append(user.getUserName()); + sb.append(", role=").append(role.getName()); + sb.append('}'); + return sb.toString(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/AuthorizationMatrixRepository.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/AuthorizationMatrixRepository.java new file mode 100644 index 0000000..25d2fb9 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/AuthorizationMatrixRepository.java @@ -0,0 +1,13 @@ +package de.thpeetz.kontor.admin.data; + +import java.util.List; +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AuthorizationMatrixRepository extends JpaRepository { + + List findByUser(User user); + + List findByRole(Role role); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/MailAccountRepository.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/MailAccountRepository.java new file mode 100644 index 0000000..ffe66ae --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/MailAccountRepository.java @@ -0,0 +1,8 @@ +package de.thpeetz.kontor.admin.data; + +import de.thpeetz.kontor.mailclient.data.MailAccount; +import org.springframework.data.jpa.repository.JpaRepository; + + +public interface MailAccountRepository extends JpaRepository { +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataColumn.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataColumn.java new file mode 100644 index 0000000..f09f087 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataColumn.java @@ -0,0 +1,37 @@ +package de.thpeetz.kontor.admin.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Table( + indexes = @Index(columnList = "columnName, table_id"), + uniqueConstraints = @UniqueConstraint(columnNames = {"table_id", "columnOrder"}) +) +public class MetaDataColumn extends AbstractEntity { + + @NotNull + private String columnName; + + private String columnSyncName; + + private String columnType; + + @Nullable + private String columnModifier; + + private Integer columnOrder; + + private Boolean isShown; + + @ManyToOne + @JoinColumn(name = "table_id") + @NotNull + private MetaDataTable table; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataColumnRepository.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataColumnRepository.java new file mode 100644 index 0000000..8959dd2 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataColumnRepository.java @@ -0,0 +1,10 @@ +package de.thpeetz.kontor.admin.data; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface MetaDataColumnRepository extends JpaRepository { + + List findByTable(MetaDataTable table); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataTable.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataTable.java new file mode 100644 index 0000000..639666d --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataTable.java @@ -0,0 +1,26 @@ +package de.thpeetz.kontor.admin.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +import java.util.LinkedList; +import java.util.List; + +@Entity +@Getter +@Setter +@Table( + indexes = @Index(columnList = "tableName"), + uniqueConstraints = @UniqueConstraint(columnNames = {"tableName"}) +) +public class MetaDataTable extends AbstractEntity { + + @NotNull + private String tableName; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "table") + private List tableColumns = new LinkedList<>(); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataTableRepository.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataTableRepository.java new file mode 100644 index 0000000..694ee52 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/MetaDataTableRepository.java @@ -0,0 +1,8 @@ +package de.thpeetz.kontor.admin.data; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MetaDataTableRepository extends JpaRepository { + + MetaDataTable findByTableName(String tableName); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/ModuleData.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/ModuleData.java new file mode 100644 index 0000000..b884425 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/ModuleData.java @@ -0,0 +1,25 @@ +package de.thpeetz.kontor.admin.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Table( + indexes = @Index(columnList = "moduleName"), + uniqueConstraints = @UniqueConstraint(columnNames = {"moduleName"}) +) +public class ModuleData extends AbstractEntity { + + @NotEmpty + private String moduleName; + + private Boolean importData; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/ModuleDataRepository.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/ModuleDataRepository.java new file mode 100644 index 0000000..484dde3 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/ModuleDataRepository.java @@ -0,0 +1,15 @@ +package de.thpeetz.kontor.admin.data; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface ModuleDataRepository extends JpaRepository { + + @Query("select m from ModuleData m where lower(m.moduleName) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + ModuleData findByModuleName(String moduleName); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/Role.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/Role.java new file mode 100644 index 0000000..5ad81c2 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/Role.java @@ -0,0 +1,30 @@ +package de.thpeetz.kontor.admin.data; + +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +@Getter +@Setter +@ToString +@Entity +public class Role extends AbstractEntity { + + @NotEmpty + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "role") + @Nullable + private List matrix; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/RoleRepository.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/RoleRepository.java new file mode 100644 index 0000000..932ae29 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/RoleRepository.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.admin.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface RoleRepository extends JpaRepository { + + @Query("select r from Role r " + + "where lower(r.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + @Query("select r from Role r " + + "where lower(r.name) like lower(:name) ") + Role findByName(@Param("name") String name); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/User.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/User.java new file mode 100644 index 0000000..a80b6bc --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/User.java @@ -0,0 +1,62 @@ +package de.thpeetz.kontor.admin.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; +import java.util.LinkedList; +import java.util.List; + +@Slf4j +@Getter +@Setter +@ToString +@Entity +@Table(indexes = @Index(columnList = "userName"), uniqueConstraints = @UniqueConstraint(columnNames = {"userName"})) +public class User extends AbstractEntity { + + private String firstName; + + private String lastName; + + @NotEmpty + private String userName; + + private String email; + + private String password; + + private boolean enabled; + + private boolean tokenExpired; + + private String token; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "user") + @Nullable + private List matrix = new LinkedList<>(); + + public String getFullName() { + StringBuilder fullNamBuilder = new StringBuilder(); + if (firstName != null) { + fullNamBuilder.append(firstName); + } + if (lastName != null) { + if (fullNamBuilder.length() > 0) { + fullNamBuilder.append(" "); + } + fullNamBuilder.append(lastName); + } + return fullNamBuilder.toString(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/data/UserRepository.java b/springboot/src/main/java/de/thpeetz/kontor/admin/data/UserRepository.java new file mode 100644 index 0000000..21d93e0 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/data/UserRepository.java @@ -0,0 +1,16 @@ +package de.thpeetz.kontor.admin.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface UserRepository extends JpaRepository { + + @Query("select u from User u " + + "where lower(u.lastName) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + User findByUserName(String userName); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/services/AdminService.java b/springboot/src/main/java/de/thpeetz/kontor/admin/services/AdminService.java new file mode 100644 index 0000000..77a6448 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/services/AdminService.java @@ -0,0 +1,110 @@ +package de.thpeetz.kontor.admin.services; + +import java.util.Collection; +import java.util.List; + +import org.springframework.stereotype.Service; + +import de.thpeetz.kontor.admin.data.AuthorizationMatrix; +import de.thpeetz.kontor.admin.data.AuthorizationMatrixRepository; +import de.thpeetz.kontor.admin.data.Role; +import de.thpeetz.kontor.admin.data.RoleRepository; +import de.thpeetz.kontor.admin.data.User; +import de.thpeetz.kontor.admin.data.UserRepository; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class AdminService { + + private final UserRepository userRepository; + private final RoleRepository roleRepository; + private final AuthorizationMatrixRepository authorizationMatrixRepository; + + public AdminService(UserRepository userRepository, RoleRepository roleRepository, + AuthorizationMatrixRepository authorizationMatrixRepository) { + this.userRepository = userRepository; + this.roleRepository = roleRepository; + this.authorizationMatrixRepository = authorizationMatrixRepository; + } + + public List findAllUsers() { + return userRepository.findAll(); + } + + public List findAllRoles() { + return roleRepository.findAll(); + } + + public Collection findAllRoles(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return roleRepository.findAll(); + } else { + return roleRepository.search(stringFilter); + } + } + + public Role addRole(String roleName) { + Role role = roleRepository.findByName(roleName); + if (role == null) { + log.info("Role {} was not found, will create it.", roleName); + role = new Role(); + role.setName(roleName); + roleRepository.save(role); + } + return role; + } + + public void saveRole(Role role) { + if (role == null) { + log.warn("Role is null. Can't save it."); + } + log.info("saveRole: role={}", role); + roleRepository.save(role); + } + + public void deleteRole(Role role) { + if (role == null) { + log.warn("Role is null. Can't delete it."); + } + log.info("deleteRole: role={}", role); + roleRepository.delete(role); + } + + public List findAllAuthorizationMatrices() { + return authorizationMatrixRepository.findAll(); + } + + public void saveAuthorizationMatrix(AuthorizationMatrix authorizationMatrix) { + if (authorizationMatrix == null) { + log.warn("AuthorizationMatrix is null. Can't save it."); + } + log.info("saveAuthorizationMatrix: authorizationMatrix={}", authorizationMatrix); + authorizationMatrixRepository.save(authorizationMatrix); + } + + public void deleteAuthorizationMatrix(AuthorizationMatrix authorizationMatrix) { + if (authorizationMatrix == null) { + log.warn("AuthorizationMatrix is null. Can't delete it."); + } + log.info("deleteAuthorizationMatrix: authorizationMatrix={}", authorizationMatrix); + authorizationMatrixRepository.delete(authorizationMatrix); + } + + public String getUserFullName(String userName) { + log.debug("get Fullname für user {}", userName); + User user = userRepository.findByUserName(userName); + if (user == null) { + log.info("keinen Eintrag für {} gefunden", userName); + return userName; + } else { + log.info("Voller Name des User {}: {}", userName, user.getFullName()); + return user.getFullName(); + } + } + + public User getUser(String userName) { + log.debug("get User {}", userName); + return userRepository.findByUserName(userName); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/services/KontorUserDetailsService.java b/springboot/src/main/java/de/thpeetz/kontor/admin/services/KontorUserDetailsService.java new file mode 100644 index 0000000..f76d6df --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/services/KontorUserDetailsService.java @@ -0,0 +1,125 @@ +package de.thpeetz.kontor.admin.services; + +import de.thpeetz.kontor.admin.data.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service("userDetailsService") +public class KontorUserDetailsService implements UserDetailsService { + + private static SecureRandom random = new SecureRandom(); + + @Autowired + private UserRepository userRepository; + + @Autowired + private RoleRepository roleRepository; + + @Autowired + private AuthorizationMatrixRepository authorizationMatrixRepository; + + public Collection findAllUsers(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return userRepository.findAll(); + } else { + return userRepository.search(stringFilter); + } + } + + public void saveUser(User user) { + if (user == null) { + log.warn("User is null. Can't save it."); + } + log.info("saveUser: user={}", user); + userRepository.save(user); + } + + public void saveUser(User user, List roles) { + if (user == null) { + log.warn("User is null. Can't save it."); + } + log.info("First save user: {}", user); + user = userRepository.save(user); + List copy = roles.stream().collect(Collectors.toList()); + List permissions = user.getMatrix(); + permissions.forEach(matrix -> { + if (roles.contains(matrix.getRole())) { + log.info("Role {} already assigned", matrix.getRole()); + copy.remove(matrix.getRole()); + } else { + log.info("Role {} has to be removed", matrix.getRole()); + authorizationMatrixRepository.delete(matrix); + } + }); + log.info("remaining roles: {}", copy); + for (Role role : copy) { + AuthorizationMatrix matrix = new AuthorizationMatrix(); + matrix.setUser(user); + matrix.setRole(role); + authorizationMatrixRepository.save(matrix); + } + } + + public void deleteUser(User user) { + if (user == null) { + log.warn("User is null. Can't delete it."); + } + log.info("deleteUser: user={}", user); + userRepository.delete(user); + } + + @Override + public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { + + log.info("loadUserByUsername: userName={}", userName); + User user = userRepository.findByUserName(userName); + if (user == null) { + log.info("User not found"); + return null; + } + + Collection authorities = getAuthorities(user); + log.info("User {} hat Rolen: {}", userName, authorities); + return new org.springframework.security.core.userdetails.User(user.getUserName(), user.getPassword(), + authorities); + } + + private Collection getAuthorities(User user) { + return authorizationMatrixRepository.findByUser(user).stream() + .map(matrix -> matrix.getRole().getName()) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } + + public String getRememberedUser(String id) { + log.info("getRememberedUser: id={}", id); + return "admin"; + } + + public String rememberUser(String username) { + String randomId = new BigInteger(130, random).toString(32); + log.info("rememberUser: username={}", username); + return randomId; + } + + public void removeRememberedUser(String id) { + log.info("removeRememberedUser: id={}", id); + } + + public List findAllRoles() { + return roleRepository.findAll(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/services/MailService.java b/springboot/src/main/java/de/thpeetz/kontor/admin/services/MailService.java new file mode 100644 index 0000000..dbe9270 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/services/MailService.java @@ -0,0 +1,24 @@ +package de.thpeetz.kontor.admin.services; + +import de.thpeetz.kontor.mailclient.data.MailAccount; +import de.thpeetz.kontor.admin.data.MailAccountRepository; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class MailService { + + private final MailAccountRepository mailAccountRepository; + + public MailService(MailAccountRepository mailAccountRepository) { + this.mailAccountRepository = mailAccountRepository; + } + + public List findAllMailAccounts() { + return mailAccountRepository.findAll(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java b/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java new file mode 100644 index 0000000..293c4d4 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java @@ -0,0 +1,101 @@ +package de.thpeetz.kontor.admin.services; + +import org.springframework.stereotype.Service; + +import de.thpeetz.kontor.admin.data.MetaDataColumn; +import de.thpeetz.kontor.admin.data.MetaDataColumnRepository; +import de.thpeetz.kontor.admin.data.MetaDataTable; +import de.thpeetz.kontor.admin.data.MetaDataTableRepository; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +@Service +public class MetaDataService { + + private final MetaDataTableRepository metaDataTableRepository; + + private final MetaDataColumnRepository metaDataColumnRepository; + + public MetaDataService(MetaDataTableRepository metaDataTableRepository, MetaDataColumnRepository metaDataColumnRepository) { + this.metaDataTableRepository = metaDataTableRepository; + this.metaDataColumnRepository = metaDataColumnRepository; + } + + public MetaDataTable getTable(String tableName) { + MetaDataTable table = metaDataTableRepository.findByTableName(tableName); + if (table == null) { + log.info("Metadata for table {} not found, will create it", tableName); + table = new MetaDataTable(); + table.setTableName(tableName); + metaDataTableRepository.save(table); + } + return table; + } + + public void getColumn(MetaDataTable table, String columnName, String columnSyncName, String columnType, String columnModifier, Integer columnOrder) { + if (table.getTableColumns().stream().anyMatch(column -> column.getColumnName().equals(columnName))) { + log.info("Column {} with name {} of table {} found, check Values", columnOrder, columnName, table.getTableName()); + MetaDataColumn column = table.getTableColumns().get(columnOrder.intValue()-1); + if (!column.getColumnName().equals(columnName)) { + log.debug("columnName has to be changed to {}", columnName); + column.setColumnName(columnName); + } + if (!column.getColumnSyncName().equals(columnSyncName)) { + log.debug("columnSyncName has to be changed to {}", columnSyncName); + column.setColumnSyncName(columnSyncName); + } + if (!column.getColumnType().equals(columnType)) { + log.debug("columnType has to be changed to {}", columnType); + column.setColumnType(columnType); + } + if (columnModifier != null && !column.getColumnModifier().equals(columnModifier)) { + log.debug("columnModifier has to be changed to {}", columnModifier); + column.setColumnModifier(columnModifier); + } + if (column.getIsShown() == null) { + log.debug("isShown set to false"); + column.setIsShown(Boolean.FALSE); + } + metaDataColumnRepository.save(column); + } else { + log.info("Column {} of table {} not found, will create it", columnName, table.getTableName()); + MetaDataColumn column = new MetaDataColumn(); + column.setTable(table); + column.setColumnName(columnName); + column.setColumnSyncName(columnSyncName); + column.setColumnType(columnType); + column.setColumnModifier(columnModifier); + column.setColumnOrder(columnOrder); + column.setIsShown(Boolean.FALSE); + metaDataColumnRepository.save(column); + } + } + + public List findAllMetaDataColumns() { + return metaDataColumnRepository.findAll(); + } + + public void deleteMetaDataColumn(MetaDataColumn metaDataColumn) { + if (metaDataColumn == null) { + log.warn("MetaDataColumn is null, can't delete it"); + return; + } + log.debug("deleteMetaDataColumn: MetaDataColumn={}", metaDataColumn); + metaDataColumnRepository.delete(metaDataColumn); + } + + public void saveMetaDataColumn(MetaDataColumn metaDataColumn) { + if (metaDataColumn == null) { + log.warn("MetaDataColumn is null, can't save it"); + return; + } + log.debug("saveMetaDataColumn: MetaDataColumn={}", metaDataColumn); + metaDataColumnRepository.save(metaDataColumn); + } + + public List findAllTables() { + return metaDataTableRepository.findAll(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/services/ModuleService.java b/springboot/src/main/java/de/thpeetz/kontor/admin/services/ModuleService.java new file mode 100644 index 0000000..7596701 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/services/ModuleService.java @@ -0,0 +1,76 @@ +package de.thpeetz.kontor.admin.services; + +import de.thpeetz.kontor.admin.data.ModuleData; +import de.thpeetz.kontor.admin.data.ModuleDataRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +public class ModuleService { + + private final ModuleDataRepository moduleDataRepository; + + public ModuleService(ModuleDataRepository moduleDataRepository) { + this.moduleDataRepository = moduleDataRepository; + } + + public List findAll(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return moduleDataRepository.findAll(); + } else { + return moduleDataRepository.search(stringFilter); + } + } + + public ModuleData findByName(String moduleName) { + if (moduleName == null || moduleName.isEmpty()) { + return null; + } else { + return moduleDataRepository.findByModuleName(moduleName); + } + } + + public boolean importData(String moduleName) { + ModuleData module = moduleDataRepository.findByModuleName(moduleName); + if (module != null) { + return module.getImportData(); + } else { + log.info("Module {} not found, should import data", moduleName); + return true; + } + } + + public void setDataImported(String moduleName) { + ModuleData module = moduleDataRepository.findByModuleName(moduleName); + if (module == null) { + log.info("Module {} not found, will create it", moduleName); + module = new ModuleData(); + module.setModuleName(moduleName); + module.setImportData(false); + moduleDataRepository.save(module); + } else { + log.info("Module {} found, change import data", module); + module.setImportData(false); + moduleDataRepository.save(module); + } + } + + public void saveModuleData(ModuleData moduleData) { + if (moduleData == null) { + log.warn("ModuleData is null, can't save it."); + } else { + moduleDataRepository.save(moduleData); + } + } + + public void deleteModuleData(ModuleData moduleData) { + if (moduleData == null) { + log.warn("ModuleData is null, can't delete it."); + } else { + moduleDataRepository.delete(moduleData); + } + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/AdminLayout.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/AdminLayout.java new file mode 100644 index 0000000..5ed32c5 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/AdminLayout.java @@ -0,0 +1,36 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.AdminConstants; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.common.views.KontorLayoutUtil; +import de.thpeetz.kontor.security.SecurityService; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class AdminLayout extends AppLayout { + + private final AdminService adminService; + + private final SecurityService securityService; + + public AdminLayout(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + + KontorLayoutUtil layout = new KontorLayoutUtil(this, adminService, securityService); + layout.setSecondaryNavigation(getSecondaryNavigation()); + layout.createHeader(AdminConstants.ADMIN_TITLE); + } + + private HorizontalLayout getSecondaryNavigation() { + HorizontalLayout navigation = new HorizontalLayout(); + navigation.addClassNames(LumoUtility.JustifyContent.CENTER, LumoUtility.Gap.SMALL, LumoUtility.Height.MEDIUM); + navigation.add(AdminConstants.getUserNavigation(), AdminConstants.getRoleNavigation(), + AdminConstants.getAuthorizationNavigation()); + return navigation; + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/AuthorizationForm.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/AuthorizationForm.java new file mode 100644 index 0000000..da593f8 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/AuthorizationForm.java @@ -0,0 +1,112 @@ +package de.thpeetz.kontor.admin.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.admin.data.AuthorizationMatrix; +import de.thpeetz.kontor.admin.data.Role; +import de.thpeetz.kontor.admin.data.User; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class AuthorizationForm extends FormLayout { + + ComboBox user = new ComboBox<>("User"); + ComboBox role = new ComboBox<>("Role"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(AuthorizationMatrix.class); + + public AuthorizationForm(List users, List roles) { + addClassName("authorizationmatrix-form"); + binder.bindInstanceFields(this); + + user.setItems(users); + user.setItemLabelGenerator(User::getUserName); + role.setItems(roles); + role.setItemLabelGenerator(Role::getName); + add(user, role, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setAuthorizationMatrix(AuthorizationMatrix authorizationMatrix) { + binder.setBean(authorizationMatrix); + } + + public abstract static class AuthorizationFormEvent extends ComponentEvent { + private AuthorizationMatrix authorizationMatrix; + + protected AuthorizationFormEvent(AuthorizationForm source, AuthorizationMatrix authorizationMatrix) { + super(source, false); + this.authorizationMatrix = authorizationMatrix; + } + + public AuthorizationMatrix getAuthorizationMatrix() { + return authorizationMatrix; + } + } + + public static class SaveEvent extends AuthorizationFormEvent { + SaveEvent(AuthorizationForm source, AuthorizationMatrix authorizationMatrix) { + super(source, authorizationMatrix); + } + } + + public static class DeleteEvent extends AuthorizationFormEvent { + DeleteEvent(AuthorizationForm source, AuthorizationMatrix authorizationMatrix) { + super(source, authorizationMatrix); + } + } + + public static class CloseEvent extends AuthorizationFormEvent { + CloseEvent(AuthorizationForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/AuthorizationView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/AuthorizationView.java new file mode 100644 index 0000000..435ae8f --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/AuthorizationView.java @@ -0,0 +1,114 @@ +package de.thpeetz.kontor.admin.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; + +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.admin.AdminConstants; +import de.thpeetz.kontor.admin.data.AuthorizationMatrix; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.RolesAllowed; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringComponent +@Scope("prototype") +@RolesAllowed("ROLE_ADMIN") +@Route(value = AdminConstants.AUTHORIZATION_ROUTE, layout = MainLayout.class) +@PageTitle("Authorization | Admin | Kontor") +public class AuthorizationView extends VerticalLayout { + + Grid grid = new Grid<>(AuthorizationMatrix.class); + AuthorizationForm form; + AdminService service; + + public AuthorizationView(AdminService service) { + this.service = service; + addClassName("authoriaztionmatrix-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("authorizationmatrix-grid"); + grid.setSizeFull(); + grid.setColumns("user.userName", "role.name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editAuthorizationMatrix(event.getValue())); + } + + private void configureForm() { + form = new AuthorizationForm(service.findAllUsers(), service.findAllRoles()); + form.setWidth("25em"); + form.addSaveListener(this::saveAuthorizationMatrix); + form.addDeleteListener(this::deleteAuthorizationMatrix); + form.addCloseListener(e -> closeEditor()); + } + + private void saveAuthorizationMatrix(AuthorizationForm.SaveEvent event) { + AuthorizationMatrix authorizationMatrix = event.getAuthorizationMatrix(); + service.saveAuthorizationMatrix(authorizationMatrix); + updateList(); + closeEditor(); + } + + private void deleteAuthorizationMatrix(AuthorizationForm.DeleteEvent event) { + service.deleteAuthorizationMatrix(event.getAuthorizationMatrix()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + Button addAuthorizationMaxtrixButton = new Button("Add permssion", click -> addAuthorizationMatrix()); + HorizontalLayout toolbar = new HorizontalLayout(addAuthorizationMaxtrixButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editAuthorizationMatrix(AuthorizationMatrix authorizationMatrix) { + if (authorizationMatrix == null) { + closeEditor(); + } else { + form.setAuthorizationMatrix(authorizationMatrix); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setAuthorizationMatrix(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addAuthorizationMatrix() { + grid.asSingleSelect().clear(); + editAuthorizationMatrix(new AuthorizationMatrix()); + } + + private void updateList() { + grid.setItems(service.findAllAuthorizationMatrices()); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/LoginView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/LoginView.java new file mode 100644 index 0000000..286bc03 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/LoginView.java @@ -0,0 +1,51 @@ +package de.thpeetz.kontor.admin.views; + +import org.springframework.security.core.context.SecurityContextHolder; + +import com.vaadin.flow.component.html.H1; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.login.LoginForm; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.BeforeEnterObserver; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.auth.AnonymousAllowed; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Route("login") +@PageTitle("Login | Vaadin Kontor") +@AnonymousAllowed +public class LoginView extends VerticalLayout implements BeforeEnterObserver { + + private final LoginForm login = new LoginForm(); + + public LoginView() { + addClassName("login-view"); + setSizeFull(); + setAlignItems(Alignment.CENTER); + setJustifyContentMode(JustifyContentMode.CENTER); + + login.setAction("login"); + + add(new H1("Vaadin Kontor")); + add(new Span("Username: user, Password: password")); + add(new Span("Username: admin, Password: password")); + add(login); + } + + @Override + public void beforeEnter(BeforeEnterEvent beforeEnterEvent) { + log.info("beforeEnter: {}", beforeEnterEvent.getLocation()); + log.info("beforeEnter: {}", SecurityContextHolder.getContext().getAuthentication()); + // inform the user about an authentication error + if (beforeEnterEvent.getLocation() + .getQueryParameters() + .getParameters() + .containsKey("error")) { + login.setError(true); + } + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java new file mode 100644 index 0000000..f70c161 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java @@ -0,0 +1,113 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.IntegerField; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.admin.data.MetaDataColumn; +import de.thpeetz.kontor.admin.data.MetaDataTable; + +import java.util.List; + +public class MetaDataForm extends FormLayout { + + ComboBox table = new ComboBox<>("Table"); + TextField columnName = new TextField("Column Name"); + TextField columnSyncName = new TextField("Column Sync Name"); + TextField columnModifier = new TextField("Column Modifier"); + IntegerField columnOrder = new IntegerField("Column Order"); + Checkbox isShown = new Checkbox("Is Shown"); + + Button save = new com.vaadin.flow.component.button.Button("Save"); + Button delete = new com.vaadin.flow.component.button.Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(MetaDataColumn.class); + + public MetaDataForm(List tables) { + addClassName("metaData-form"); + binder.bindInstanceFields(this); + + table.setItems(tables); + table.setItemLabelGenerator(MetaDataTable::getTableName); + add(table, columnName, columnSyncName, columnModifier, columnOrder, isShown, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new MetaDataForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new MetaDataForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new MetaDataForm.SaveEvent(this, binder.getBean())); + } + } + + public void setMetaDataColumn(MetaDataColumn metaDataColumn) { + binder.setBean(metaDataColumn); + } + + public abstract static class MetaDataFormEvent extends ComponentEvent { + private MetaDataColumn metaDataColumn; + + protected MetaDataFormEvent(MetaDataForm source, MetaDataColumn metaDataColumn) { + super(source, false); + this.metaDataColumn = metaDataColumn; + } + + public MetaDataColumn getMetaDataColumn() { + return metaDataColumn; + } + } + + public static class SaveEvent extends MetaDataForm.MetaDataFormEvent { + SaveEvent(MetaDataForm source, MetaDataColumn metaDataColumn) { + super(source, metaDataColumn); + } + } + + public static class DeleteEvent extends MetaDataForm.MetaDataFormEvent { + DeleteEvent(MetaDataForm source, MetaDataColumn metaDataColumn) { + super(source, metaDataColumn); + } + } + + public static class CloseEvent extends MetaDataForm.MetaDataFormEvent { + CloseEvent(MetaDataForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(MetaDataForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(MetaDataForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(MetaDataForm.CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java new file mode 100644 index 0000000..661d4a3 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java @@ -0,0 +1,112 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.admin.AdminConstants; +import de.thpeetz.kontor.admin.data.MetaDataColumn; +import de.thpeetz.kontor.admin.services.MetaDataService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.RolesAllowed; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Scope; + +@Slf4j +@SpringComponent +@Scope("prototype") +@RolesAllowed("ROLE_ADMIN") +@Route(value = AdminConstants.METADATA_ROUTE, layout = MainLayout.class) +@PageTitle("Meta Data | Admin | Kontor") +public class MetaDataView extends VerticalLayout { + + Grid grid = new Grid<>(MetaDataColumn.class); + MetaDataForm form; + MetaDataService service; + + public MetaDataView(MetaDataService service) { + this.service = service; + addClassName("metadata-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("metadata-grid"); + grid.setSizeFull(); + grid.setColumns("table.tableName", "columnName", "columnSyncName", "columnModifier", "columnOrder", "isShown"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editMetaData(event.getValue())); + } + + private void configureForm() { + form = new MetaDataForm(service.findAllTables()); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveMetaData); + form.addDeleteListener(this::deleteMetaData); + form.addCloseListener(e -> closeEditor()); + } + + private void saveMetaData(MetaDataForm.SaveEvent event) { + MetaDataColumn metaDataColumn = event.getMetaDataColumn(); + service.saveMetaDataColumn(metaDataColumn); + updateList(); + closeEditor(); + } + + private void deleteMetaData(MetaDataForm.DeleteEvent event) { + service.deleteMetaDataColumn(event.getMetaDataColumn()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + Button addAuthorizationMaxtrixButton = new Button("Add Meta Data", click -> addMetaDataColumn()); + HorizontalLayout toolbar = new HorizontalLayout(addAuthorizationMaxtrixButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editMetaData(MetaDataColumn metaDataColumn) { + if (metaDataColumn == null) { + closeEditor(); + } else { + form.setMetaDataColumn(metaDataColumn); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setMetaDataColumn(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addMetaDataColumn() { + grid.asSingleSelect().clear(); + editMetaData(new MetaDataColumn()); + } + + private void updateList() { + grid.setItems(service.findAllMetaDataColumns()); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataForm.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataForm.java new file mode 100644 index 0000000..b2c5f8c --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataForm.java @@ -0,0 +1,100 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.admin.data.ModuleData; + +public class ModuleDataForm extends FormLayout { + TextField moduleName = new TextField("Module Name"); + Checkbox importData = new Checkbox("Import Data"); + + Button save = new com.vaadin.flow.component.button.Button("Save"); + Button delete = new com.vaadin.flow.component.button.Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(ModuleData.class); + + public ModuleDataForm() { + addClassName("moduleData-form"); + binder.bindInstanceFields(this); + add(moduleName, importData, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new ModuleDataForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new ModuleDataForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new ModuleDataForm.SaveEvent(this, binder.getBean())); + } + } + + public void setModuleData(ModuleData moduleData) { + binder.setBean(moduleData); + } + + public abstract static class ModuleDataFormEvent extends ComponentEvent { + private ModuleData moduleData; + + protected ModuleDataFormEvent(ModuleDataForm source, ModuleData moduleData) { + super(source, false); + this.moduleData = moduleData; + } + + public ModuleData getModuleData() { + return moduleData; + } + } + + public static class SaveEvent extends ModuleDataForm.ModuleDataFormEvent { + SaveEvent(ModuleDataForm source, ModuleData moduleData) { + super(source, moduleData); + } + } + + public static class DeleteEvent extends ModuleDataForm.ModuleDataFormEvent { + DeleteEvent(ModuleDataForm source, ModuleData moduleData) { + super(source, moduleData); + } + } + + public static class CloseEvent extends ModuleDataForm.ModuleDataFormEvent { + CloseEvent(ModuleDataForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(ModuleDataForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(ModuleDataForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(ModuleDataForm.CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataView.java new file mode 100644 index 0000000..bc335f3 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataView.java @@ -0,0 +1,118 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.admin.data.ModuleData; +import de.thpeetz.kontor.admin.services.ModuleService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.RolesAllowed; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Scope; + +@Slf4j +@SpringComponent +@Scope("prototype") +@RolesAllowed("ROLE_ADMIN") +@Route(value = "admin/module", layout = MainLayout.class) +@PageTitle("Module Data | Admin | Kontor") +public class ModuleDataView extends VerticalLayout { + + Grid grid = new Grid<>(ModuleData.class); + TextField filterText = new TextField(); + ModuleDataForm form; + ModuleService service; + + public ModuleDataView(ModuleService service) { + this.service = service; + addClassName("moduleData-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("moduleData-grid"); + grid.setSizeFull(); + grid.setColumns("moduleName", "importData"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editModuleData(event.getValue())); + } + + private void configureForm() { + form = new ModuleDataForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveModuleData); + form.addDeleteListener(this::deleteModuleData); + form.addCloseListener(e -> closeEditor()); + } + + private void saveModuleData(ModuleDataForm.SaveEvent event) { + ModuleData moduleData = event.getModuleData(); + service.saveModuleData(moduleData); + updateList(); + closeEditor(); + } + + private void deleteModuleData(ModuleDataForm.DeleteEvent event) { + service.deleteModuleData(event.getModuleData()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by module name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + Button addModuleDataButton = new Button("Add module", click -> addModuleData()); + HorizontalLayout toolbar = new HorizontalLayout(filterText, addModuleDataButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editModuleData(ModuleData moduleData) { + if (moduleData == null) { + closeEditor(); + } else { + form.setModuleData(moduleData); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setModuleData(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addModuleData() { + grid.asSingleSelect().clear(); + editModuleData(new ModuleData()); + } + + private void updateList() { + grid.setItems(service.findAll(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/RoleForm.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/RoleForm.java new file mode 100644 index 0000000..671d0f5 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/RoleForm.java @@ -0,0 +1,102 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.admin.data.Role; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class RoleForm extends FormLayout { + + TextField name = new TextField("Role name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Role.class); + + public RoleForm() { + addClassName("role-form"); + binder.bindInstanceFields(this); + add(name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setRole(Role role) { + binder.setBean(role); + } + + public abstract static class RoleFormEvent extends ComponentEvent { + private Role role; + + protected RoleFormEvent(RoleForm source, Role role) { + super(source, false); + this.role = role; + } + + public Role getRole() { + return role; + } + } + + public static class SaveEvent extends RoleFormEvent { + SaveEvent(RoleForm source, Role role) { + super(source, role); + } + } + + public static class DeleteEvent extends RoleFormEvent { + DeleteEvent(RoleForm source, Role role) { + super(source, role); + } + } + + public static class CloseEvent extends RoleFormEvent { + CloseEvent(RoleForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/RoleView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/RoleView.java new file mode 100644 index 0000000..1f13c9a --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/RoleView.java @@ -0,0 +1,118 @@ +package de.thpeetz.kontor.admin.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.admin.AdminConstants; +import de.thpeetz.kontor.admin.data.Role; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.RolesAllowed; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringComponent +@Scope("prototype") +@RolesAllowed("ROLE_ADMIN") +@Route(value = AdminConstants.ROLE_ROUTE, layout = MainLayout.class) +@PageTitle("Rollen | Admin | Kontor") +public class RoleView extends VerticalLayout { + Grid grid = new Grid<>(Role.class); + TextField filterText = new TextField(); + RoleForm form; + AdminService service; + + public RoleView(AdminService service) { + this.service = service; + addClassName("user-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("user-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editRole(event.getValue())); + } + + private void configureForm() { + form = new RoleForm(); + form.setWidth("25em"); + form.addSaveListener(this::saveRole); + form.addDeleteListener(this::deleteRole); + form.addCloseListener(e -> closeEditor()); + } + + private void saveRole(RoleForm.SaveEvent event) { + service.saveRole(event.getRole()); + updateList(); + closeEditor(); + } + + private void deleteRole(RoleForm.DeleteEvent event) { + service.deleteRole(event.getRole()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by user name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + Button addUserButton = new Button("Add user", click -> addUser()); + HorizontalLayout toolbar = new HorizontalLayout(filterText, addUserButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editRole(Role role) { + if (role == null) { + closeEditor(); + } else { + form.setRole(role); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setRole(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addUser() { + grid.asSingleSelect().clear(); + editRole(new Role()); + } + + private void updateList() { + grid.setItems(service.findAllRoles(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/UserForm.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/UserForm.java new file mode 100644 index 0000000..21298f3 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/UserForm.java @@ -0,0 +1,143 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.checkbox.CheckboxGroup; +import com.vaadin.flow.component.checkbox.CheckboxGroupVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.EmailField; +import com.vaadin.flow.component.textfield.PasswordField; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.admin.data.Role; +import de.thpeetz.kontor.admin.data.User; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class UserForm extends FormLayout { + + TextField userName = new TextField("User name"); + PasswordField password = new PasswordField("Password"); + EmailField email = new EmailField("Email"); + TextField firstName = new TextField("First name"); + TextField lastName = new TextField("Last name"); + Checkbox enabled = new Checkbox("Enabled"); + String originalPassword; + + CheckboxGroup permissions = new CheckboxGroup<>("Permissions"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(User.class); + + public UserForm() { + addClassName("user-form"); + binder.bindInstanceFields(this); + add(userName, password, email, firstName, lastName, enabled, configurePermissionsGroup(), createButtonsLayout()); + } + + private CheckboxGroup configurePermissionsGroup() { + permissions.addThemeVariants(CheckboxGroupVariant.LUMO_VERTICAL); + permissions.setItemLabelGenerator(Role::getName); + permissions.addValueChangeListener(event -> { + log.debug("permissions changed: {}", event); + }); + return permissions; + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setUser(User user) { + binder.setBean(user); + //log.debug("UserForm.setUser: {}", user); + if (user != null) { + this.originalPassword = user.getPassword(); + } else { + this.originalPassword = null; + } + } + + public void setRoles(List roles, User user) { + permissions.setItems(roles); + user.getMatrix().stream().forEach(authorizationMatrix -> { + permissions.select(authorizationMatrix.getRole()); + }); + } + + public boolean hasPasswordChanged(User user) { + return !originalPassword.equals(user.getPassword()); + } + + public abstract static class UserFormEvent extends ComponentEvent { + private User user; + + protected UserFormEvent(UserForm source, User user) { + super(source, false); + this.user = user; + } + + public User getUser() { + return user; + } + } + + public static class SaveEvent extends UserFormEvent { + SaveEvent(UserForm source, User user) { + super(source, user); + } + } + + public static class DeleteEvent extends UserFormEvent { + DeleteEvent(UserForm source, User user) { + super(source, user); + } + } + + public static class CloseEvent extends UserFormEvent { + CloseEvent(UserForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/UserProfileView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/UserProfileView.java new file mode 100644 index 0000000..b2a9ef8 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/UserProfileView.java @@ -0,0 +1,73 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import de.thpeetz.kontor.admin.data.User; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.security.SecurityService; +import jakarta.annotation.security.PermitAll; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Route(value="user/profile", layout = MainLayout.class) +@PermitAll +@PageTitle("Profile | User | Kontor") +public class UserProfileView extends VerticalLayout { + + private SecurityService securityService; + + private AdminService adminService; + + TextField firstName = new TextField("First name"); + TextField lastName = new TextField("Last name"); + + Button save = new Button("Save"); + Button close = new Button("Cancel"); + Binder binder = new BeanValidationBinder<>(User.class); + + public UserProfileView(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + binder.bindInstanceFields(this); + + add(firstName, lastName, createButtonsLayout()); + securityService.getAuthenticatedUser().ifPresent(user -> { + log.info("UserProfileView: {}", user.getUsername()); + binder.setBean(adminService.getUser(user.getUsername())); + }); + + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + close.addClickListener(event -> closeView()); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + log.info("update user profile"); + } + } + + private void closeView() { + log.info("close user profile view"); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/UserView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/UserView.java new file mode 100644 index 0000000..9f981ec --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/UserView.java @@ -0,0 +1,135 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.admin.data.Role; +import de.thpeetz.kontor.admin.data.User; +import de.thpeetz.kontor.admin.services.KontorUserDetailsService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.RolesAllowed; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@SpringComponent +@Scope("prototype") +@RolesAllowed("ROLE_ADMIN") +@Route(value = "admin/user", layout = MainLayout.class) +@PageTitle("User | Admin | Kontor") +public class UserView extends VerticalLayout { + + Grid grid = new Grid<>(User.class); + TextField filterText = new TextField(); + UserForm form; + KontorUserDetailsService service; + + @Autowired + PasswordEncoder passwordEncoder; + + public UserView(KontorUserDetailsService service) { + this.service = service; + addClassName("user-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("user-grid"); + grid.setSizeFull(); + grid.setColumns("userName", "email", "firstName", "lastName", "enabled"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editUser(event.getValue())); + } + + private void configureForm() { + form = new UserForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveUser); + form.addDeleteListener(this::deleteUser); + form.addCloseListener(e -> closeEditor()); + } + + private void saveUser(UserForm.SaveEvent event) { + User user = event.getUser(); + log.debug("UserView.saveUser: {}", user); + List permissions = form.permissions.getSelectedItems().stream().collect(Collectors.toList()); + log.info("selected permissions: {}", permissions); + if (form.hasPasswordChanged(user)) { + user.setPassword(passwordEncoder.encode(user.getPassword())); + log.debug("password changed for user {}", user); + } + service.saveUser(user, permissions); + updateList(); + closeEditor(); + } + + private void deleteUser(UserForm.DeleteEvent event) { + service.deleteUser(event.getUser()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by user name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + Button addUserButton = new Button("Add user", click -> addUser()); + HorizontalLayout toolbar = new HorizontalLayout(filterText, addUserButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editUser(User user) { + if (user == null) { + closeEditor(); + } else { + form.setUser(user); + form.setRoles(service.findAllRoles(), user); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setUser(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addUser() { + grid.asSingleSelect().clear(); + editUser(new User()); + } + + private void updateList() { + grid.setItems(service.findAllUsers(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/BookshelfConstants.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/BookshelfConstants.java new file mode 100644 index 0000000..58661a0 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/BookshelfConstants.java @@ -0,0 +1,53 @@ +package de.thpeetz.kontor.bookshelf; + +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.sidenav.SideNav; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.RouterLink; + +import de.thpeetz.kontor.bookshelf.views.ArticleView; +import de.thpeetz.kontor.bookshelf.views.AuthorView; +import de.thpeetz.kontor.bookshelf.views.BookView; +import de.thpeetz.kontor.bookshelf.views.BookshelfPublisherView; + +public class BookshelfConstants { + + public static final String AUTHOR = "Author"; + public static final String ARTICLE = "Article"; + public static final String BOOK = "Book"; + public static final String BOOKSHELF = "Bookshelf"; + public static final String PUBLISHER = "Publisher"; + public static final String PUBLISHER_ROUTE = "bookshelf/publisher"; + public static final String AUTHOR_ROUTE = "bookshelf/author"; + public static final String BOOK_ROUTE = "bookshelf/book"; + public static final String ARTICLE_ROUTE = "bookshelf/article"; + + public static RouterLink getPublisherLink() { + return new RouterLink(PUBLISHER, BookshelfPublisherView.class); + } + + public static RouterLink getAuthorLink() { + return new RouterLink(AUTHOR, AuthorView.class); + } + + public static RouterLink getBookLink() { + return new RouterLink(BOOK, BookView.class); + } + + public static RouterLink getArticleLink() { + return new RouterLink(ARTICLE, ArticleView.class); + } + + public static SideNavItem getBookshelfNavigation() { + SideNavItem bookshelf = new SideNavItem(BOOKSHELF, BookView.class, VaadinIcon.OPEN_BOOK.create()); + bookshelf.addItem(new SideNavItem(BOOK, BookView.class)); + bookshelf.addItem(new SideNavItem(PUBLISHER, BookshelfPublisherView.class)); + bookshelf.addItem(new SideNavItem(AUTHOR, AuthorView.class)); + bookshelf.addItem(new SideNavItem(ARTICLE, ArticleView.class)); + return bookshelf; + } + + private BookshelfConstants() { + // private constructor to hide the implicit public one + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/SetupModuleBookshelf.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/SetupModuleBookshelf.java new file mode 100644 index 0000000..abb0e9a --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/SetupModuleBookshelf.java @@ -0,0 +1,82 @@ +package de.thpeetz.kontor.bookshelf; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +import de.thpeetz.kontor.admin.services.ModuleService; +import de.thpeetz.kontor.bookshelf.data.ArticleAuthorRepository; +import de.thpeetz.kontor.bookshelf.data.Author; +import de.thpeetz.kontor.bookshelf.data.AuthorRepository; +import de.thpeetz.kontor.bookshelf.data.BookAuthorRepository; +import de.thpeetz.kontor.bookshelf.data.BookRepository; +import de.thpeetz.kontor.bookshelf.data.BookshelfPublisher; +import de.thpeetz.kontor.bookshelf.data.BookshelfPublisherRepository; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class SetupModuleBookshelf implements ApplicationListener { + + boolean alreadySetup = false; + + @Autowired + private BookshelfPublisherRepository publisherRepository; + + @Autowired + private AuthorRepository authorRepository; + + @Autowired + private ArticleAuthorRepository articleAuthorRepository; + + @Autowired + private BookRepository bookRepository; + + @Autowired + private BookAuthorRepository bookAuthorRepository; + + @Autowired + private ModuleService moduleService; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + if (alreadySetup) { + log.info("SetupModuleBookshelf already executed, skipping"); + return; + } + if (!moduleService.importData(BookshelfConstants.BOOKSHELF)) { + log.info("Module Bookshelf should not setup data"); + return; + } + + log.info("Set up Bookshelf data"); + Author douglasadams = createAuthorIfNotFound("Douglas", "Adams"); + moduleService.setDataImported(BookshelfConstants.BOOKSHELF); + } + + private Author createAuthorIfNotFound(String firstName, String lastName) { + log.info("createAuthorIfNotFound {} {}", firstName, lastName); + Author author = authorRepository.findByFirstNameAndLastName(firstName, lastName); + if (author == null) { + log.info("Author {} {} not found, will create it", firstName, lastName); + author = new Author(); + author.setFirstName(firstName); + author.setLastName(lastName); + authorRepository.save(author); + } + return author; + } + + private BookshelfPublisher createPublisherIfNotFound(String publisherName) { + log.info("createPublisherIfNotFound {}", publisherName); + BookshelfPublisher publisher = publisherRepository.findByName(publisherName); + if (publisher == null) { + log.info("Publisher {} not found, will create it", publisherName); + publisher = new BookshelfPublisher(); + publisher.setName(publisherName); + publisherRepository.save(publisher); + } + return publisher; + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Article.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Article.java new file mode 100644 index 0000000..3ee6a16 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Article.java @@ -0,0 +1,27 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.LinkedList; +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@Entity +@Table(indexes = @Index(columnList = "title"), uniqueConstraints = @UniqueConstraint(columnNames = {"title"})) +public class Article extends AbstractEntity { + + @NotEmpty + private String title; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "article") + @Nullable + private List authors = new LinkedList<>(); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthor.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthor.java new file mode 100644 index 0000000..c2b3851 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthor.java @@ -0,0 +1,29 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(indexes = @Index(columnList = "author_id, article_id"), uniqueConstraints = @UniqueConstraint(columnNames = {"author_id", "article_id"})) +public class ArticleAuthor extends AbstractEntity { + + @ManyToOne + @JoinColumn(name = "author_id") + @NotNull + private Author author; + + @ManyToOne + @JoinColumn(name = "article_id") + @NotNull + private Article article; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepository.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepository.java new file mode 100644 index 0000000..d7933de --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepository.java @@ -0,0 +1,12 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ArticleAuthorRepository extends JpaRepository { + + List findByAuthor(Author author); + + List findByArticle(Article article); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleRepository.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleRepository.java new file mode 100644 index 0000000..c6b2602 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleRepository.java @@ -0,0 +1,16 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface ArticleRepository extends JpaRepository { + + @Query("select a from Article a " + + "where lower(a.title) like lower(concat('%', :searchTerm, '%'))") + List

search(@Param("searchTerm") String searchTerm); + + List
findByTitle(String title); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Author.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Author.java new file mode 100644 index 0000000..42d83e0 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Author.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.LinkedList; +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@Entity +@Table(indexes = { + @Index(columnList = "firstName, lastName"), + @Index(columnList = "lastName, firstName") +}, uniqueConstraints = { + @UniqueConstraint(columnNames = { "firstName", "lastName" }) +}) +public class Author extends AbstractEntity { + + @NotEmpty + private String firstName; + + @NotEmpty + private String lastName; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "author") + @Nullable + private List bookAuthors = new LinkedList<>(); + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "author") + @Nullable + private List articleAuthors = new LinkedList<>(); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/AuthorRepository.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/AuthorRepository.java new file mode 100644 index 0000000..3e87334 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/AuthorRepository.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface AuthorRepository extends JpaRepository { + + @Query("select a from Author a " + + "where lower(a.firstName) like lower(concat('%', :searchTerm, '%')) " + + "or lower(a.lastName) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); + + Author findByFirstNameAndLastName(String firstName, String lastName); + +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Book.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Book.java new file mode 100644 index 0000000..e0f6782 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/Book.java @@ -0,0 +1,42 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.LinkedList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@Entity +@Table(indexes = @Index(columnList = "title, isbn"), uniqueConstraints = @UniqueConstraint(columnNames = {"isbn"})) +public class Book extends AbstractEntity { + + @NotEmpty + private String title; + + @NotEmpty + private String isbn; + + @Nullable + private int year; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "book") + @Nullable + private List authors = new LinkedList<>(); + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "publisher_id") + @NotNull + @JsonIgnoreProperties({ "books" }) + private BookshelfPublisher publisher; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthor.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthor.java new file mode 100644 index 0000000..86a0e10 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthor.java @@ -0,0 +1,29 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(indexes = @Index(columnList = "author_id, book_id"), uniqueConstraints = @UniqueConstraint(columnNames = {"author_id", "book_id"})) +public class BookAuthor extends AbstractEntity { + + @ManyToOne + @JoinColumn(name = "author_id") + @NotNull + private Author author; + + @ManyToOne + @JoinColumn(name = "book_id") + @NotNull + private Book book; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepository.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepository.java new file mode 100644 index 0000000..a0cf650 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepository.java @@ -0,0 +1,12 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BookAuthorRepository extends JpaRepository { + + List findByAuthor(Author author); + + List findByBook(Book book); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookRepository.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookRepository.java new file mode 100644 index 0000000..e6ee013 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookRepository.java @@ -0,0 +1,21 @@ +package de.thpeetz.kontor.bookshelf.data; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface BookRepository extends JpaRepository { + + @Query("select b from Book b " + + "where lower(b.title) like lower(concat('%', :searchTerm, '%')) " + + "or lower(b.isbn) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); + + List findByTitle(String name); + + List findByTitleIgnoreCase(String name); + + List findByIsbn(String isbn); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisher.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisher.java new file mode 100644 index 0000000..da97d7a --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisher.java @@ -0,0 +1,32 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.LinkedList; +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@Entity +@Table(indexes = @Index(columnList = "name"), uniqueConstraints = @UniqueConstraint(columnNames = { "name" })) +public class BookshelfPublisher extends AbstractEntity { + + @NotEmpty + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "publisher", orphanRemoval = true) + @Nullable + List books = new LinkedList<>(); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepository.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepository.java new file mode 100644 index 0000000..b50ba84 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepository.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface BookshelfPublisherRepository extends JpaRepository { + + @Query("select p from BookshelfPublisher p " + + "where lower(p.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + BookshelfPublisher findByName(String name); + + List findByNameIgnoreCase(String name); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/services/BookshelfService.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/services/BookshelfService.java new file mode 100644 index 0000000..6d92a2c --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/services/BookshelfService.java @@ -0,0 +1,118 @@ +package de.thpeetz.kontor.bookshelf.services; + +import java.util.List; + +import de.thpeetz.kontor.bookshelf.data.*; +import org.springframework.stereotype.Service; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class BookshelfService { + + private final AuthorRepository authorRepository; + private final ArticleAuthorRepository articleAuthorRepository; + private final ArticleRepository articleRepository; + private final BookRepository bookRepository; + private final BookAuthorRepository bookAuthorRepository; + private final BookshelfPublisherRepository publisherRepository; + + public BookshelfService(AuthorRepository authorRepository, ArticleAuthorRepository articleAuthorRepository, + ArticleRepository articleRepository, BookRepository bookRepository, + BookAuthorRepository bookAuthorRepository, BookshelfPublisherRepository publisherRepository) { + this.authorRepository = authorRepository; + this.articleAuthorRepository = articleAuthorRepository; + this.articleRepository = articleRepository; + this.bookRepository = bookRepository; + this.bookAuthorRepository = bookAuthorRepository; + this.publisherRepository = publisherRepository; + } + + public List findAllPublishers(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return publisherRepository.findAll(); + } else { + return publisherRepository.search(stringFilter); + } + } + + public BookshelfPublisher findPublisherByName(String publisherName) { + return publisherRepository.findByName(publisherName); + } + + public void deletePublisher(BookshelfPublisher publisher) { + publisherRepository.delete(publisher); + } + + public void savePublisher(BookshelfPublisher publisher) { + if (publisher == null) { + log.warn("Publisher is null. Are you sure you have connected your form to the application?"); + return; + } + publisherRepository.save(publisher); + } + + public List findAllAuthors(String filter) { + if (filter == null || filter.isEmpty()) { + return authorRepository.findAll(); + } else { + return authorRepository.search(filter); + } + } + + public void saveAuthor(Author author) { + if (author == null) { + log.warn("Author is null. Are you sure you have connected your form to the application?"); + return; + } + authorRepository.save(author); + } + + public void deleteAuthor(Author author) { + authorRepository.delete(author); + } + + public List findAllBooks(String filter) { + if (filter == null || filter.isEmpty()) { + return bookRepository.findAll(); + } else { + return bookRepository.search(filter); + } + } + + public void saveBook(Book book) { + if (book == null) { + log.warn("Book is null. Are you sure you have connected your form to the application?"); + return; + } + bookRepository.save(book); + } + + public void deleteBook(Book book) { + BookshelfPublisher publisher = book.getPublisher(); + publisher.getBooks().remove(book); + publisherRepository.save(publisher); + bookRepository.delete(book); + } + + public List
findAllArticles(String filter) { + if (filter == null || filter.isEmpty()) { + return articleRepository.findAll(); + } else { + return articleRepository.search(filter); + } + } + + public void saveArticle(Article article) { + if (article == null) { + log.warn("Article is null, Are you sure you have connected your form to the application?"); + return; + } + articleRepository.save(article); + } + + public void deleteArticle(Article article) { + articleRepository.delete(article); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleForm.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleForm.java new file mode 100644 index 0000000..7fb7958 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleForm.java @@ -0,0 +1,106 @@ +package de.thpeetz.kontor.bookshelf.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.bookshelf.data.Article; +import de.thpeetz.kontor.bookshelf.data.Author; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class ArticleForm extends FormLayout { + + TextField title = new TextField("Title"); + Grid author = new Grid<>(Author.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder
binder = new BeanValidationBinder<>(Article.class); + + public ArticleForm() { + addClassName("article-form"); + binder.bindInstanceFields(this); + + add(title, author, createButtonsLayout()); + } + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new ArticleForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new ArticleForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new ArticleForm.SaveEvent(this, binder.getBean())); + } + } + + public void setArticle(Article article) { + binder.setBean(article); + } + + public abstract static class ArticleFormEvent extends ComponentEvent { + private Article article; + + protected ArticleFormEvent(ArticleForm source, Article article) { + super(source, false); + this.article = article; + } + + public Article getArticle() { + return article; + } + } + + public static class SaveEvent extends ArticleForm.ArticleFormEvent { + SaveEvent(ArticleForm source, Article article) { + super(source, article); + } + } + + public static class DeleteEvent extends ArticleForm.ArticleFormEvent { + DeleteEvent(ArticleForm source, Article article) { + super(source, article); + } + } + + public static class CloseEvent extends ArticleForm.ArticleFormEvent { + CloseEvent(ArticleForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(ArticleForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(ArticleForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(ArticleForm.CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleView.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleView.java new file mode 100644 index 0000000..334f577 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleView.java @@ -0,0 +1,126 @@ +package de.thpeetz.kontor.bookshelf.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.bookshelf.data.Article; +import de.thpeetz.kontor.bookshelf.data.Book; +import de.thpeetz.kontor.bookshelf.data.BookAuthor; +import de.thpeetz.kontor.bookshelf.services.BookshelfService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; +import lombok.Getter; +import org.springframework.context.annotation.Scope; + +import java.util.stream.Collectors; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = BookshelfConstants.ARTICLE_ROUTE, layout = MainLayout.class) +@PageTitle("Article | Bookshelf | Kontor") +public class ArticleView extends VerticalLayout { + + @Getter + Grid
grid = new Grid<>(Article.class); + TextField filterText = new TextField(); + @Getter + ArticleForm form; + BookshelfService service; + + public ArticleView(BookshelfService service) { + this.service = service; + addClassName("article-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("article-grid"); + grid.setSizeFull(); + grid.setColumns("title"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editArticle(event.getValue())); + } + + private void configureForm() { + form = new ArticleForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveArticle); + form.addDeleteListener(this::deleteArticle); + form.addCloseListener(e -> closeEditor()); + } + + private void saveArticle(ArticleForm.SaveEvent event) { + service.saveArticle(event.getArticle()); + updateList(); + closeEditor(); + } + + private void deleteArticle(ArticleForm.DeleteEvent event) { + service.deleteArticle(event.getArticle()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by title or isbn..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addArticleButton = new Button("Add article"); + addArticleButton.addClickListener(click -> addArticle()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addArticleButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editArticle(Article article) { + if (article == null) { + closeEditor(); + } else { + form.setArticle(article); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setArticle(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addArticle() { + grid.asSingleSelect().clear(); + editArticle(new Article()); + } + + public void updateList() { + grid.setItems(service.findAllArticles(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorForm.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorForm.java new file mode 100644 index 0000000..34d0f5d --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorForm.java @@ -0,0 +1,117 @@ +package de.thpeetz.kontor.bookshelf.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.listbox.ListBox; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.bookshelf.data.Author; +import de.thpeetz.kontor.bookshelf.data.Book; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class AuthorForm extends FormLayout { + + TextField firstName = new TextField("First Name"); + TextField lastName = new TextField("Last Name"); + ListBox books = new ListBox<>(); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Author.class); + + public AuthorForm() { + addClassName("author-form"); + binder.bindInstanceFields(this); + + books.setHeight("100px"); + //books.setColumns("title", "publisher.name"); + //books.getColumns().forEach(col -> col.setAutoWidth(true)); + add(firstName, lastName, books, createButtonsLayout()); + } + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new AuthorForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new AuthorForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new AuthorForm.SaveEvent(this, binder.getBean())); + } + } + + public void setAuthor(Author author) { + binder.setBean(author); + } + + public void setBooks(List books) { + log.info("setting Books: ", books); + this.books.setItems(books); + } + + public abstract static class AuthorFormEvent extends ComponentEvent { + private Author author; + + protected AuthorFormEvent(AuthorForm source, Author author) { + super(source, false); + this.author = author; + } + + public Author getAuthor() { + return author; + } + } + + public static class SaveEvent extends AuthorForm.AuthorFormEvent { + SaveEvent(AuthorForm source, Author author) { + super(source, author); + } + } + + public static class DeleteEvent extends AuthorForm.AuthorFormEvent { + DeleteEvent(AuthorForm source, Author author) { + super(source, author); + } + } + + public static class CloseEvent extends AuthorForm.AuthorFormEvent { + CloseEvent(AuthorForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(AuthorForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(AuthorForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(AuthorForm.CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorView.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorView.java new file mode 100644 index 0000000..17897b0 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorView.java @@ -0,0 +1,132 @@ +package de.thpeetz.kontor.bookshelf.views; + +import java.util.stream.Collectors; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.bookshelf.data.Author; +import de.thpeetz.kontor.bookshelf.data.BookAuthor; +import de.thpeetz.kontor.bookshelf.services.BookshelfService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = BookshelfConstants.AUTHOR_ROUTE, layout = MainLayout.class) +@PageTitle("Author | Bookshelf | Kontor") +public class AuthorView extends VerticalLayout { + + Grid grid = new Grid<>(Author.class); + TextField filterText = new TextField(); + AuthorForm form; + BookshelfService service; + + public AuthorView(BookshelfService service) { + this.service = service; + addClassName("author-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + public AuthorForm getForm() { + return form; + } + + private void configureGrid() { + grid.addClassName("author-grid"); + grid.setSizeFull(); + grid.setColumns("firstName", "lastName"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editAuthor(event.getValue())); + } + + private void configureForm() { + form = new AuthorForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveAuthor); + form.addDeleteListener(this::deleteAuthor); + form.addCloseListener(e -> closeEditor()); + } + private void saveAuthor(AuthorForm.SaveEvent event) { + service.saveAuthor(event.getAuthor()); + updateList(); + closeEditor(); + } + + private void deleteAuthor(AuthorForm.DeleteEvent event) { + service.deleteAuthor(event.getAuthor()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addAuthorButton = new Button("Add author"); + addAuthorButton.addClickListener(click -> addAuthor()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addAuthorButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editAuthor(Author author) { + if (author == null) { + closeEditor(); + } else { + form.setAuthor(author); + form.setBooks(author.getBookAuthors().stream().map(BookAuthor::getBook).collect(Collectors.toList())); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setAuthor(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addAuthor() { + grid.asSingleSelect().clear(); + editAuthor(new Author()); + } + + public void updateList() { + grid.setItems(service.findAllAuthors(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookForm.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookForm.java new file mode 100644 index 0000000..61da274 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookForm.java @@ -0,0 +1,111 @@ +package de.thpeetz.kontor.bookshelf.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.IntegerField; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.bookshelf.data.Book; +import de.thpeetz.kontor.bookshelf.data.BookshelfPublisher; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class BookForm extends FormLayout { + + TextField title = new TextField("Title"); + TextField isbn = new TextField("ISBN"); + IntegerField year = new IntegerField("Year"); + ComboBox publisher = new ComboBox<>("Publisher"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Book.class); + + public BookForm(List publishers) { + addClassName("book-form"); + binder.bindInstanceFields(this); + + publisher.setItems(publishers); + publisher.setItemLabelGenerator(BookshelfPublisher::getName); + add(title, isbn, year, publisher, createButtonsLayout()); + } + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new BookForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new BookForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new BookForm.SaveEvent(this, binder.getBean())); + } + } + + public void setBook(Book book) { + binder.setBean(book); + } + + public abstract static class BookFormEvent extends ComponentEvent { + private Book book; + + protected BookFormEvent(BookForm source, Book book) { + super(source, false); + this.book = book; + } + + public Book getBook() { + return book; + } + } + + public static class SaveEvent extends BookForm.BookFormEvent { + SaveEvent(BookForm source, Book book) { + super(source, book); + } + } + + public static class DeleteEvent extends BookForm.BookFormEvent { + DeleteEvent(BookForm source, Book book) { + super(source, book); + } + } + + public static class CloseEvent extends BookForm.BookFormEvent { + CloseEvent(BookForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(BookForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(BookForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(BookForm.CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookView.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookView.java new file mode 100644 index 0000000..9e04dc6 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookView.java @@ -0,0 +1,124 @@ +package de.thpeetz.kontor.bookshelf.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.bookshelf.data.Book; +import de.thpeetz.kontor.bookshelf.services.BookshelfService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; +import lombok.Getter; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = BookshelfConstants.BOOK_ROUTE, layout = MainLayout.class) +@PageTitle("Book | Bookshelf | Kontor") +public class BookView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(Book.class); + TextField filterText = new TextField(); + @Getter + BookForm form; + BookshelfService service; + + public BookView(BookshelfService service) { + this.service = service; + addClassName("book-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("book-grid"); + grid.setSizeFull(); + grid.setColumns("title", "isbn", "publisher.name", "year"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editBook(event.getValue())); + } + + private void configureForm() { + form = new BookForm(service.findAllPublishers(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveBook); + form.addDeleteListener(this::deleteBook); + form.addCloseListener(e -> closeEditor()); + } + + private void saveBook(BookForm.SaveEvent event) { + service.saveBook(event.getBook()); + updateList(); + closeEditor(); + } + + private void deleteBook(BookForm.DeleteEvent event) { + service.deleteBook(event.getBook()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by title or isbn..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addBookButton = new Button("Add book"); + addBookButton.addClickListener(click -> addBook()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addBookButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editBook(Book book) { + if (book == null) { + closeEditor(); + } else { + form.setBook(book); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setBook(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addBook() { + grid.asSingleSelect().clear(); + editBook(new Book()); + } + + public void updateList() { + grid.setItems(service.findAllBooks(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfLayout.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfLayout.java new file mode 100644 index 0000000..6105a47 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfLayout.java @@ -0,0 +1,34 @@ +package de.thpeetz.kontor.bookshelf.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.common.views.KontorLayoutUtil; +import de.thpeetz.kontor.security.SecurityService; + +public class BookshelfLayout extends AppLayout { + + private final AdminService adminService; + + private final SecurityService securityService; + + public BookshelfLayout(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + + KontorLayoutUtil layout = new KontorLayoutUtil(this, adminService, securityService); + layout.setSecondaryNavigation(getSecondaryNavigation()); + layout.createHeader(BookshelfConstants.BOOKSHELF); + } + + private HorizontalLayout getSecondaryNavigation() { + HorizontalLayout navigation = new HorizontalLayout(); + navigation.addClassNames(LumoUtility.JustifyContent.CENTER, LumoUtility.Gap.SMALL, LumoUtility.Height.MEDIUM); + navigation.add(BookshelfConstants.getBookLink(), BookshelfConstants.getArticleLink(), + BookshelfConstants.getPublisherLink(), BookshelfConstants.getAuthorLink()); + return navigation; + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfPublisherView.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfPublisherView.java new file mode 100644 index 0000000..de5f9b5 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfPublisherView.java @@ -0,0 +1,131 @@ +package de.thpeetz.kontor.bookshelf.views; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.common.views.SeparateMainLayout; +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.bookshelf.data.BookshelfPublisher; +import de.thpeetz.kontor.bookshelf.services.BookshelfService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = BookshelfConstants.PUBLISHER_ROUTE, layout = MainLayout.class) +@PageTitle("Publisher | Bookshelf | Kontor") +public class BookshelfPublisherView extends VerticalLayout { + + Grid grid = new Grid<>(BookshelfPublisher.class); + TextField filterText = new TextField(); + PublisherForm form; + + BookshelfService service; + + public BookshelfPublisherView(BookshelfService service) { + this.service = service; + addClassName("publisher-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("publisher-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editPublisher(event.getValue())); + } + + private void configureForm() { + form = new PublisherForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::savePublisher); + form.addDeleteListener(this::deletePublisher); + form.addCloseListener(e -> closeEditor()); + } + + private void savePublisher(PublisherForm.SaveEvent event) { + service.savePublisher(event.getPublisher()); + updateList(); + closeEditor(); + } + + private void deletePublisher(PublisherForm.DeleteEvent event) { + service.deletePublisher(event.getPublisher()); + updateList(); + closeEditor(); + } + + public Grid getGrid() { + return grid; + } + + public PublisherForm getForm() { + return form; + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addPublisherButton = new Button("Add publisher"); + addPublisherButton.addClickListener(click -> addPublisher()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addPublisherButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editPublisher(BookshelfPublisher publisher) { + if (publisher == null) { + closeEditor(); + } else { + form.setPublisher(publisher); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setPublisher(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addPublisher() { + grid.asSingleSelect().clear(); + editPublisher(new BookshelfPublisher()); + } + + public void updateList() { + grid.setItems(service.findAllPublishers(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/PublisherForm.java b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/PublisherForm.java new file mode 100644 index 0000000..7b48122 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/bookshelf/views/PublisherForm.java @@ -0,0 +1,108 @@ +package de.thpeetz.kontor.bookshelf.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.bookshelf.data.BookshelfPublisher; + +public class PublisherForm extends FormLayout { + private TextField name = new TextField("Name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder(BookshelfPublisher.class); + + public PublisherForm() { + addClassName("publisher-form"); + binder.bindInstanceFields(this); + + add(name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public TextField getName() { + return name; + } + + public void setName(TextField name) { + this.name = name; + } + + public void setPublisher(BookshelfPublisher publisher) { + binder.setBean(publisher); + } + + public abstract static class PublisherFormEvent extends ComponentEvent { + private BookshelfPublisher publisher; + + protected PublisherFormEvent(PublisherForm source, BookshelfPublisher publisher) { + super(source, false); + this.publisher = publisher; + } + + public BookshelfPublisher getPublisher() { + return publisher; + } + } + + public static class SaveEvent extends PublisherFormEvent { + SaveEvent(PublisherForm source, BookshelfPublisher publisher) { + super(source, publisher); + } + } + + public static class DeleteEvent extends PublisherFormEvent { + DeleteEvent(PublisherForm source, BookshelfPublisher publisher) { + super(source, publisher); + } + } + + public static class CloseEvent extends PublisherFormEvent { + CloseEvent(PublisherForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/ComicConstants.java b/springboot/src/main/java/de/thpeetz/kontor/comics/ComicConstants.java new file mode 100644 index 0000000..c89f7b1 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/ComicConstants.java @@ -0,0 +1,93 @@ +package de.thpeetz.kontor.comics; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.RouterLink; + +import de.thpeetz.kontor.comics.views.ArtistView; +import de.thpeetz.kontor.comics.views.ComicView; +import de.thpeetz.kontor.comics.views.ComicWorkView; +import de.thpeetz.kontor.comics.views.IssueView; +import de.thpeetz.kontor.comics.views.PublisherView; +import de.thpeetz.kontor.comics.views.StoryArcView; +import de.thpeetz.kontor.comics.views.TradePaperbackView; +import de.thpeetz.kontor.comics.views.VolumeView; +import de.thpeetz.kontor.comics.views.WorktypeView; + +/** + * The {@code ComicConstants} class contains constant values related to comics. + */ +public class ComicConstants { + + public static final String COMICS = "Comics"; + public static final String COMICS_ROUTE = "comics/comic"; + public static final String PUBLISHER = "Publisher"; + public static final String PUBLISHER_ROUTE = "comics/publisher"; + public static final String COMICWORK = "ComicWork"; + public static final String COMICWORK_ROUTE = "comics/comicwork"; + public static final String WORKTYPE = "Worktype"; + public static final String WORKTYPE_ROUTE = "comics/worktype"; + public static final String ARTIST = "Artist"; + public static final String ARTIST_ROUTE = "comics/artist"; + public static final String TPB = "TradePaperback"; + public static final String TPB_ROUTE = "comics/tradepaperback"; + public static final String STORYARC = "Story Arc"; + public static final String STORYARC_ROTE = "comics/storyarc"; + public static final String ISSUE = "Issue"; + public static final String ISSUE_ROUTE = "comics/issue"; + public static final String VOLUME = "Volume"; + public static final String VOLUME_ROUTE = "comics/volume"; + + public static RouterLink getArtistLink() { + return new RouterLink(ARTIST, ArtistView.class); + } + + public static RouterLink getComicLink() { + return new RouterLink(COMICS, ComicView.class); + } + + public static RouterLink getPublisherLink() { + return new RouterLink(PUBLISHER, PublisherView.class); + } + + public static RouterLink getComicWorkLink() { + return new RouterLink(COMICWORK, ComicWorkView.class); + } + + public static RouterLink getIssueLink() { + return new RouterLink(ISSUE, IssueView.class); + } + + public static RouterLink getTradePaperbackLink() { + return new RouterLink(TPB, TradePaperbackView.class); + } + + public static Component getStoryArcLink() { + return new RouterLink(STORYARC, StoryArcView.class); + } + + public static RouterLink getWorktypeLink() { + return new RouterLink(WORKTYPE, WorktypeView.class); + } + + public static RouterLink getVolumeLink() { + return new RouterLink(VOLUME, VolumeView.class); + } + + public static SideNavItem getComicsNavigation() { + SideNavItem comics = new SideNavItem(COMICS, COMICS_ROUTE, VaadinIcon.RECORDS.create()); + comics.addItem(new SideNavItem(ARTIST, ArtistView.class)); + comics.addItem(new SideNavItem(COMICS, ComicView.class)); + comics.addItem(new SideNavItem(PUBLISHER, PublisherView.class)); + comics.addItem(new SideNavItem(ISSUE, IssueView.class)); + comics.addItem(new SideNavItem(TPB, TradePaperbackView.class)); + comics.addItem(new SideNavItem(STORYARC, StoryArcView.class)); + comics.addItem(new SideNavItem(VOLUME, VolumeView.class)); + return comics; + } + + private ComicConstants() { + // private constructor to hide the implicit public one + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/SetupModuleComics.java b/springboot/src/main/java/de/thpeetz/kontor/comics/SetupModuleComics.java new file mode 100644 index 0000000..52674fd --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/SetupModuleComics.java @@ -0,0 +1,1198 @@ +package de.thpeetz.kontor.comics; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +import de.thpeetz.kontor.admin.services.ModuleService; +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.ArtistRepository; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.ComicRepository; +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.data.ComicWorkRepository; +import de.thpeetz.kontor.comics.data.Issue; +import de.thpeetz.kontor.comics.data.IssueRepository; +import de.thpeetz.kontor.comics.data.Publisher; +import de.thpeetz.kontor.comics.data.PublisherRepository; +import de.thpeetz.kontor.comics.data.StoryArc; +import de.thpeetz.kontor.comics.data.StoryArcRepository; +import de.thpeetz.kontor.comics.data.TradePaperback; +import de.thpeetz.kontor.comics.data.TradePaperbackRepository; +import de.thpeetz.kontor.comics.data.Volume; +import de.thpeetz.kontor.comics.data.VolumeRepository; +import de.thpeetz.kontor.comics.data.Worktype; +import de.thpeetz.kontor.comics.data.WorktypeRepository; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class SetupModuleComics implements ApplicationListener { + + boolean alreadySetup = false; + + @Autowired + private PublisherRepository publisherRepository; + + @Autowired + private ArtistRepository artistRepository; + + @Autowired + private WorktypeRepository worktypeRepository; + + @Autowired + private ComicRepository comicRepository; + + @Autowired + private ComicWorkRepository comicWorkRepository; + + @Autowired + private StoryArcRepository storyArcRepository; + + @Autowired + private TradePaperbackRepository tradePaperbackRepository; + + @Autowired + private IssueRepository issueRepository; + + @Autowired + private VolumeRepository volumeRepository; + + @Autowired + private ModuleService moduleService; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + if (alreadySetup) { + log.info("SetupModuleComics already executed, skipping"); + return; + } + if (!moduleService.importData(ComicConstants.COMICS)) { + log.info("Module comics should not setup data"); + return; + } + log.info("Set up Comics data"); + Publisher marvel = createPublisherIfNotFound("Marvel"); + Publisher alias = createPublisherIfNotFound("Alias"); + Publisher crossgen = createPublisherIfNotFound("Crossgen"); + Publisher image = createPublisherIfNotFound("Image"); + Publisher ddp = createPublisherIfNotFound("Devils Due Publishing"); + Publisher aspen = createPublisherIfNotFound("Aspen"); + Publisher bongo = createPublisherIfNotFound("Bongo Comics"); + Publisher kandora = createPublisherIfNotFound("Kandora"); + Publisher dc = createPublisherIfNotFound("DC"); + Publisher marvelknights = createPublisherIfNotFound("Marvel Knights"); + Publisher wildstorm = createPublisherIfNotFound("WildStorm"); + Publisher cliffhanger = createPublisherIfNotFound("Cliffhanger"); + Publisher darkhorse = createPublisherIfNotFound("Dark Horse Comics"); + Publisher broadsword = createPublisherIfNotFound("Broadsword"); + Publisher dynamite = createPublisherIfNotFound("Dynamite Entertainment"); + Publisher redeagle = createPublisherIfNotFound("Red Eagle Entertainment"); + Publisher topcow = createPublisherIfNotFound("Top Cow Productions"); + Publisher pulpfiction = createPublisherIfNotFound("Pulp Fiction"); + Artist michaelturner = createArtistIfNotFound("Turner, Michael"); + createArtistIfNotFound("Marz, Ron"); + createArtistIfNotFound("Whedon, Joss"); + createArtistIfNotFound("Land, Greg"); + Artist brianbendis = createArtistIfNotFound("Bendis, Brian Michael"); + Worktype writer = createWorktypeIfNotFound("Writer"); + createWorktypeIfNotFound("Penciler"); + createWorktypeIfNotFound("Inker"); + Comic comic1602 = createComicIfNotFound(marvel, "1602", false, false); + createComicIfNotFound(alias, "10th Muse", false, false); + Comic abadazad = createComicIfNotFound(crossgen, "Abadazad", false, false); + Comic amazingfantasy = createComicIfNotFound(marvel, "Amazing Fantasy", false, false); + Comic amazingspiderman = createComicIfNotFound(marvel, "Amazing Spider-Man", false, false); + Comic arana = createComicIfNotFound(marvel, "Arana", false, false); + Comic aria = createComicIfNotFound(image, "Aria", false, false); + createComicIfNotFound(ddp, "Army of Darkness", false, false); + Comic aspenComic = createComicIfNotFound(aspen, "Aspen", false, false); + Comic astonishingxmen = createComicIfNotFound(marvel, "Astonishing X-Men", false, false); + Comic athena = createComicIfNotFound(image, "Athena Inc. The Beginning", false, false); + Comic athenamanhunter = createComicIfNotFound(image, "Athena Inc. The Manhunter Project", false, false); + Comic barbarossa = createComicIfNotFound(kandora, "Barbarossa & The Lost Corsairs", false, false); + Comic bartsimpson = createComicIfNotFound(bongo, "Bart Simpson", false, false); + Comic bartsimpsontree = createComicIfNotFound(bongo, "Bart Simpsons Treehouse of Horror", false, false); + Comic battlepope = createComicIfNotFound(image, "Battle Pope", false, false); + Comic birdsofprey = createComicIfNotFound(dc, "Birds of Prey", false, false); + Comic blackwidow = createComicIfNotFound(marvelknights, "Black Widow", false, false); + Comic blackwidow2 = createComicIfNotFound(marvelknights, "Black Widow 2", false, false); + createComicIfNotFound(image, "Bluntman and Chronic", false, false); + Comic brath = createComicIfNotFound(crossgen, "Brath", false, false); + Comic catwomanrome = createComicIfNotFound(dc, "Catwoman When In Rome", false, false); + Comic crimson = createComicIfNotFound(wildstorm, "Crimson", false, false); + Comic crossgencomic = createComicIfNotFound(crossgen, "Crossgen", false, false); + Comic dangergirl = createComicIfNotFound(cliffhanger, "Danger Girl", false, false); + Comic dangergirlbackinblack = createComicIfNotFound(wildstorm, "Danger Girl Back in Black", false, false); + Comic daringescapes = createComicIfNotFound(image, "Daring Escapes", false, true); + Comic darknesssuperman = createComicIfNotFound(image, "Darkness / Superman", false, false); + Comic darknesstombraider = createComicIfNotFound(image, "Darkness / Tomb Raider", false, false); + Comic darknessvampirella = createComicIfNotFound(image, "Darkness / Vampirella", false, false); + Comic darknessblacksails = createComicIfNotFound(image, "Darkness Black Sails", false, false); + Comic darknessvol2 = createComicIfNotFound(image, "Darkness Vol. 2", false, false); + Comic districtx = createComicIfNotFound(marvel, "District X", false, false); + Comic dragonlance = createComicIfNotFound(ddp, "Dragonlance: Chronicles", false, false); + Comic dreampolice = createComicIfNotFound(marvel, "Dream Police", false, false); + Comic elcazador = createComicIfNotFound(crossgen, "El Cazador", false, false); + Comic elcazadortom = createComicIfNotFound(crossgen, "El Cazador The Bloody Ballad of Blackjack Tom", false, + false); + Comic elsinore = createComicIfNotFound(alias, "Elsinore", false, false); + Comic emmafrost = createComicIfNotFound(marvel, "Emma Frost", false, false); + Comic excalibur = createComicIfNotFound(marvel, "Excalibur", false, false); + Comic fathom = createComicIfNotFound(aspen, "Fathom", false, false); + Comic fathombeginnings = createComicIfNotFound(aspen, "Fathom Beginnings", false, false); + Comic fathomcannon = createComicIfNotFound(aspen, "Fathom Cannon Hawke", false, false); + Comic fathomcannonprelude = createComicIfNotFound(aspen, "Fathom Cannon Hawke Prelude", false, false); + Comic fathomdawnofwar = createComicIfNotFound(aspen, "Fathom Dawn of War", false, false); + Comic fathomprelude = createComicIfNotFound(aspen, "Fathom Prelude", false, false); + Comic fathomswimsuit = createComicIfNotFound(aspen, "Fathom Swimsuit Special", false, false); + Comic fathomswimsuit2000 = createComicIfNotFound(aspen, "Fathom Swimsuit Special 2000", false, false); + Comic fathomvol2 = createComicIfNotFound(aspen, "Fathom Vol. 2", false, false); + Comic fathomkillian = createComicIfNotFound(aspen, "Fathom: Killians Tide", false, false); + Comic flakriot = createComicIfNotFound(image, "Flak Riot", false, false); + Comic freshmen = createComicIfNotFound(image, "Freshmen", false, false); + Comic friendlyneighborspider = createComicIfNotFound(marvel, "Friendly Neighborhood Spider-Man", false, false); + Comic futurama = createComicIfNotFound(bongo, "Futurama", false, false); + Comic futuramaco2 = createComicIfNotFound(bongo, "Futurama Simpsons Crossover Crisis Part 2", false, false); + Comic ghostrider = createComicIfNotFound(marvelknights, "Ghostrider", false, false); + Comic gift = createComicIfNotFound(image, "Gift", false, false); + Comic hackslashtoys = createComicIfNotFound(ddp, "Hack Slash Land of Lost Toys", false, false); + Comic hackslashgirlsgonedead = createComicIfNotFound(ddp, "Hack/Slash: Girls Gone Dead", false, false); + Comic harryjohnson = createComicIfNotFound(pulpfiction, "Harry Johnson", false, false); + Comic hellcop = createComicIfNotFound(image, "Hellcop", false, false); + Comic holidayspecial2004 = createComicIfNotFound(marvel, "Holiday Special 2004", false, false); + Comic housem = createComicIfNotFound(marvel, "House of M", false, false); + Comic hunterkiller = createComicIfNotFound(image, "Hunter-Killer", false, false); + Comic hunterkillerdossier = createComicIfNotFound(image, "Hunter-Killer Dossier", false, false); + Comic ironghost = createComicIfNotFound(image, "Iron Ghost", false, false); + Comic judge = createComicIfNotFound(image, "J.U.D.G.E.: Secret Rage", false, false); + Comic kisskissbangbang = createComicIfNotFound(crossgen, "Kiss Kiss Bang Bang", false, false); + Comic legendofisis = createComicIfNotFound(alias, "Legend of Isis", false, false); + Comic loki = createComicIfNotFound(marvel, "Loki", false, false); + Comic lullaby = createComicIfNotFound(image, "Lullaby", false, false); + Comic magdalenavampirella2 = createComicIfNotFound(topcow, "Magdalena / Vampirella 2", false, false); + Comic marvelknightspider = createComicIfNotFound(marvelknights, "Marvel Knights Spider-Man", false, false); + Comic marville = createComicIfNotFound(marvel, "Marville", false, false); + Comic maryjane = createComicIfNotFound(marvel, "Mary Jane", false, false); + Comic maryjanehome = createComicIfNotFound(marvel, "Mary Jane Homecoming", false, false); + Comic megacity = createComicIfNotFound(ddp, "Megacity 909", false, false); + Comic meridian = createComicIfNotFound(crossgen, "Meridian", false, false); + Comic midnightnation = createComicIfNotFound(image, "Midnight Nation", false, true); + Comic monsterwar = createComicIfNotFound(image, "Monster War", false, false); + Comic monsterwar2005 = createComicIfNotFound(image, "Monster War 2005", false, false); + Comic mystic = createComicIfNotFound(crossgen, "Mystic", false, false); + Comic mystique = createComicIfNotFound(marvel, "Mystique", false, false); + Comic necromancer = createComicIfNotFound(image, "Necromancer", false, false); + Comic negationwar = createComicIfNotFound(crossgen, "Negation War", false, false); + Comic newavengers = createComicIfNotFound(marvel, "New Avengers", false, false); + Comic newmutants = createComicIfNotFound(marvel, "New Mutants", false, false); + Comic newxmen = createComicIfNotFound(marvel, "New X-Men", false, false); + Comic newxmenacademy = createComicIfNotFound(marvel, "New X-Men Academy X", false, false); + Comic newxmenhellions = createComicIfNotFound(marvel, "New X-Men Hellions", false, false); + Comic nightcrawler = createComicIfNotFound(marvel, "Nightcrawler", false, false); + Comic ororo = createComicIfNotFound(marvel, "Ororo: Before the Storm", false, false); + Comic radix = createComicIfNotFound(image, "Radix", false, false); + Comic redsonja = createComicIfNotFound(dynamite, "Red Sonja", false, false); + Comic revelations = createComicIfNotFound(darkhorse, "Revelations", false, false); + Comic rogue = createComicIfNotFound(marvel, "Rogue", false, false); + Comic ruse = createComicIfNotFound(crossgen, "Ruse", false, false); + Comic samurai = createComicIfNotFound(darkhorse, "Samurai: Heaven & Earth", false, false); + Comic scion = createComicIfNotFound(crossgen, "Scion", false, false); + Comic shanna = createComicIfNotFound(marvelknights, "Shanna, The She-Devil", false, false); + Comic shehulk = createComicIfNotFound(marvel, "She-Hulk", false, false); + Comic shehulk2 = createComicIfNotFound(marvel, "She-Hulk 2", false, false); + Comic shijunen = createComicIfNotFound(darkhorse, "Shi Ju-Nen", false, false); + Comic shrek = createComicIfNotFound(darkhorse, "Shrek", false, false); + Comic simpsons = createComicIfNotFound(bongo, "Simpsons", false, false); + Comic sojourn = createComicIfNotFound(crossgen, "Sojourn", false, false); + Comic solus = createComicIfNotFound(crossgen, "Solus", false, false); + Comic soulfire = createComicIfNotFound(aspen, "Soulfire", false, false); + Comic soulfirelight = createComicIfNotFound(aspen, "Soulfire Dying of the Light", false, false); + Comic spectacularspiderman = createComicIfNotFound(marvel, "Spectacular Spider-Man", false, false); + Comic spellbinders = createComicIfNotFound(marvel, "Spellbinders", false, false); + Comic spidermanindia = createComicIfNotFound(marvel, "Spider-Man India", false, false); + Comic spidermanlovesmary = createComicIfNotFound(marvel, "Spider-Man loves Mary Jane", false, false); + Comic spidermanteam = createComicIfNotFound(marvel, "Spider-Man Team Up", false, false); + Comic spidermanbreakout = createComicIfNotFound(marvel, "Spider-Man: Breakout", false, false); + Comic spidermanhousem = createComicIfNotFound(marvel, "Spider-Man: House of M", false, false); + Comic starwars = createComicIfNotFound(darkhorse, "Star Wars", false, false); + Comic stardustkid = createComicIfNotFound(image, "Stardust Kid", false, false); + Comic strange = createComicIfNotFound(marvel, "Strange", false, false); + Comic supergirl = createComicIfNotFound(dc, "Supergirl", false, false); + Comic superman = createComicIfNotFound(dc, "Superman", false, false); + Comic supermanbatman = createComicIfNotFound(dc, "Superman/Batman", false, false); + Comic tarotblackrose = createComicIfNotFound(broadsword, "Tarot: Witch of the Black Rose", false, false); + Comic artgreghorn = createComicIfNotFound(image, "The Art of Greg Horn", false, false); + Comic devilskeeper = createComicIfNotFound(alias, "The Devil´s Keeper", false, false); + Comic thetenth = createComicIfNotFound(image, "The Tenth", false, false); + Comic tombdracula = createComicIfNotFound(marvel, "The Tomb of Dracula", false, false); + Comic robertjordanwheeloftime = createComicIfNotFound(redeagle, "Robert Jordan´s The Wheel of Time: New Spring", + false, false); + Comic thorsonasgard = createComicIfNotFound(marvel, "Thor: Son of Asgard", false, false); + Comic tomstrong = createComicIfNotFound(wildstorm, "Tom Strong", false, false); + Comic tombraider = createComicIfNotFound(image, "Tomb Raider", false, false); + Comic tombraidergreatesttreasure = createComicIfNotFound(image, "Tomb Raider: The Greatest Treasure of All", + false, false); + Comic toxin = createComicIfNotFound(marvel, "Toxin", false, false); + Comic ultimatefantasticfour = createComicIfNotFound(marvel, "Ultimate Fantastic Four", false, false); + Comic ultimatespidermanannual = createComicIfNotFound(marvel, "Ultimate Spider-Man Annual", false, false); + Comic uncannyxmen = createComicIfNotFound(marvel, "Uncanny X-Men", false, false); + Comic vampirella = createComicIfNotFound(dynamite, "Vampirella", false, false); + Comic wildgirl = createComicIfNotFound(wildstorm, "Wild Girl", false, false); + Comic wildcats = createComicIfNotFound(wildstorm, "Wildcats: Nemesis", false, false); + Comic wildsiderz = createComicIfNotFound(wildstorm, "Wildsiderz", false, false); + Comic witchblade = createComicIfNotFound(image, "Witchblade", false, false); + Comic witchbladetombraider = createComicIfNotFound(image, "Witchblade / Tomb Raider", false, false); + Comic wolverine = createComicIfNotFound(marvel, "Wolverine: The End", false, false); + Comic woodboy = createComicIfNotFound(image, "Wood Boy", false, false); + Comic wraithborn = createComicIfNotFound(wildstorm, "Wraithborn", false, false); + Comic x23 = createComicIfNotFound(marvel, "X-23", false, false); + Comic xmenageofapocalypse = createComicIfNotFound(marvel, "X-Men: Age of Apocalypse", false, false); + Comic xmenageofapocalypseoneshot = createComicIfNotFound(marvel, "X-Men: Age of Apocalypse One Shot", false, + false); + Comic xmenkittypryde = createComicIfNotFound(marvel, "X-Men: Kitty Pryde", false, false); + Comic phoenix = createComicIfNotFound(marvel, "X-Men: Phoenix - Endsong", false, false); + Comic xtremexmen = createComicIfNotFound(marvel, "X-treme X-Men", false, false); + Comic armydarknessreanim = createComicIfNotFound(dynamite, "Army of Darkness vs. Re-Animator", false, false); + Comic runaways = createComicIfNotFound(marvel, "Runaways", false, false); + Comic crux = createComicIfNotFound(crossgen, "Crux", false, false); + Comic ariasoulmarket = createComicIfNotFound(image, "Aria: The Soul Market", false, false); + Comic ariasummerspell = createComicIfNotFound(image, "Aria: Summer´s Spell", false, false); + Comic ariaenchantment = createComicIfNotFound(image, "Aria: The Uses of Enchantment", false, false); + Comic armydarknessashes = createComicIfNotFound(darkhorse, "Army of Darkness: Ashes 2 Ashes", false, false); + Comic armydarknessshop = createComicIfNotFound(darkhorse, "Army of Darkness: Shop Till You Drop Dead", false, + false); + createComicIfNotFound(marvel, "X-Men", false, false); + createComicIfNotFound(image, "Bomb Queen II: Queen of Hearts", false, false); + createComicIfNotFound(image, "Bomb Queen III: The Good, The Bad and The Lovely", false, false); + createComicIfNotFound(image, "Bomb Queen IV: Suicide Bomber", false, false); + createComicIfNotFound(wildstorm, "Gen13", false, false); + createComicIfNotFound(aspen, "Iron & The Maiden", false, false); + createComicIfNotFound(darkhorse, "Star Wars: Rebellion", false, false); + createComicIfNotFound(darkhorse, "Star Wars: Knights of the Old Republic", false, false); + createComicIfNotFound(darkhorse, "Star Wars: Legacy", false, false); + createComicIfNotFound(darkhorse, "Star Wars: Dark Times", false, false); + createComicWorkIfNotFound(crossgencomic, michaelturner, writer); + createComicWorkIfNotFound(dangergirl, michaelturner, writer); + createComicWorkIfNotFound(ultimatefantasticfour, michaelturner, writer); + createComicWorkIfNotFound(ultimatespidermanannual, michaelturner, writer); + createComicWorkIfNotFound(uncannyxmen, michaelturner, writer); + createComicWorkIfNotFound(starwars, michaelturner, writer); + createComicWorkIfNotFound(shehulk, michaelturner, writer); + createComicWorkIfNotFound(shehulk2, michaelturner, writer); + createComicWorkIfNotFound(scion, michaelturner, writer); + createComicWorkIfNotFound(newavengers, michaelturner, writer); + createComicWorkIfNotFound(newmutants, michaelturner, writer); + createComicWorkIfNotFound(midnightnation, michaelturner, writer); + createComicWorkIfNotFound(monsterwar, michaelturner, writer); + createComicWorkIfNotFound(monsterwar2005, michaelturner, writer); + createComicWorkIfNotFound(mystic, michaelturner, writer); + createComicWorkIfNotFound(holidayspecial2004, michaelturner, writer); + createComicWorkIfNotFound(hackslashgirlsgonedead, michaelturner, writer); + createComicWorkIfNotFound(uncannyxmen, brianbendis, writer); + createStoryArcIfNotFound("Higher Learning", emmafrost); + createStoryArcIfNotFound("Mind Games", emmafrost); + createStoryArcIfNotFound("Bloom", emmafrost); + createTradePaperbackIfNotFound("Vol. 1", midnightnation, 1, 12); + createTradePaperbackIfNotFound("From The Ashes", sojourn, 1, 6); + createTradePaperbackIfNotFound("The Dragons Tale", sojourn, 7, 12); + createTradePaperbackIfNotFound("The Warriors Tale", sojourn, 13, 18); + createTradePaperbackIfNotFound("The Thiefs Tale", sojourn, 19, 24); + createTradePaperbackIfNotFound("Vol. 1", runaways, 1, 18); + createTradePaperbackIfNotFound("Choices", gift, 1, 5); + createTradePaperbackIfNotFound("Atlantis Rising", crux, 1, 6); + createTradePaperbackIfNotFound("Test Of Time", crux, 7, 12); + createTradePaperbackIfNotFound("Strangers in Atlantis", crux, 13, 18); + createTradePaperbackIfNotFound("Flying Solo", meridian, 1, 7); + createTradePaperbackIfNotFound("Going To Ground", meridian, 8, 14); + createTradePaperbackIfNotFound("Taking The Skies", meridian, 15, 20); + createTradePaperbackIfNotFound("Coming Home", meridian, 21, 26); + createTradePaperbackIfNotFound("Rite of Passage", mystic, 1, 7); + createTradePaperbackIfNotFound("The Demon Queen", mystic, 8, 14); + createTradePaperbackIfNotFound("Siege of Scales", mystic, 15, 20); + createTradePaperbackIfNotFound("Out All Night", mystic, 21, 26); + createTradePaperbackIfNotFound("Single Green Female", shehulk, 1, 6); + createTradePaperbackIfNotFound("Superhuman Law", shehulk, 7, 12); + createTradePaperbackIfNotFound("Conflict of Conscience", scion, 1, 7); + createTradePaperbackIfNotFound("Blood For Blood", scion, 8, 14); + createTradePaperbackIfNotFound("Divided Loyalties", scion, 15, 21); + createTradePaperbackIfNotFound("Sanctuary", scion, 22, 27); + createTradePaperbackIfNotFound("The End Of History", uncannyxmen, 444, 449); + createTradePaperbackIfNotFound("Public Enemies", supermanbatman, 1, 6); + createTradePaperbackIfNotFound("Loyalty And Loss", crimson, 1, 6); + createTradePaperbackIfNotFound("Heaven & Earth", crimson, 7, 12); + createTradePaperbackIfNotFound("Earth Angel", crimson, 13, 18); + createTradePaperbackIfNotFound("Redemption", crimson, 19, 24); + createTradePaperbackIfNotFound("1602", comic1602, 1, 8); + createTradePaperbackIfNotFound("Coming Home", amazingspiderman, 30, 35); + createTradePaperbackIfNotFound("Revelations", amazingspiderman, 36, 39); + createTradePaperbackIfNotFound("Until the Stars Turn Cold", amazingspiderman, 40, 45); + createTradePaperbackIfNotFound("The Life & Death of Spiders", amazingspiderman, 46, 50); + createTradePaperbackIfNotFound("Unintended Consequences", amazingspiderman, 51, 56); + createTradePaperbackIfNotFound("Happy Birthday", amazingspiderman, 500, 502); + createTradePaperbackIfNotFound("Sonderband 1", dangergirl, 1, 2); + createTradePaperbackIfNotFound("Of Like Minds", birdsofprey, 56, 61); + createTradePaperbackIfNotFound("Sensei & Student", birdsofprey, 62, 68); + createIssueIfNotFound("1", phoenix, false, false); + createIssueIfNotFound("2", phoenix, false, false); + createIssueIfNotFound("3", phoenix, false, false); + createIssueIfNotFound("4", phoenix, false, false); + createIssueIfNotFound("5", phoenix, false, false); + createIssueIfNotFound("1", midnightnation, true, false); + createIssueIfNotFound("2", midnightnation, true, false); + createIssueIfNotFound("3", midnightnation, true, false); + createIssueIfNotFound("4", midnightnation, true, false); + createIssueIfNotFound("5", midnightnation, true, false); + createIssueIfNotFound("6", midnightnation, true, false); + createIssueIfNotFound("7", midnightnation, true, false); + createIssueIfNotFound("8", midnightnation, true, false); + createIssueIfNotFound("9", midnightnation, true, false); + createIssueIfNotFound("10", midnightnation, true, false); + createIssueIfNotFound("11", midnightnation, true, false); + createIssueIfNotFound("12", midnightnation, true, false); + createIssueIfNotFound("1", arana, false, false); + createIssueIfNotFound("2", arana, false, false); + createIssueIfNotFound("3", arana, false, false); + createIssueIfNotFound("4", arana, false, false); + createIssueIfNotFound("5", arana, false, false); + createIssueIfNotFound("6", arana, false, false); + createIssueIfNotFound("7", arana, false, false); + createIssueIfNotFound("8", arana, false, false); + createIssueIfNotFound("9", arana, false, false); + createIssueIfNotFound("10", arana, false, false); + createIssueIfNotFound("11", arana, false, false); + createIssueIfNotFound("1", futurama, false, false); + createIssueIfNotFound("2", futurama, false, false); + createIssueIfNotFound("3", futurama, false, false); + createIssueIfNotFound("4", futurama, false, false); + createIssueIfNotFound("5", futurama, false, false); + createIssueIfNotFound("6", futurama, false, false); + createIssueIfNotFound("7", futurama, false, false); + createIssueIfNotFound("8", futurama, false, false); + createIssueIfNotFound("9", futurama, false, false); + createIssueIfNotFound("10", futurama, false, false); + createIssueIfNotFound("11", futurama, false, false); + createIssueIfNotFound("12", futurama, false, false); + createIssueIfNotFound("13", futurama, false, false); + createIssueIfNotFound("14", futurama, false, false); + createIssueIfNotFound("15", futurama, false, false); + createIssueIfNotFound("16", futurama, false, false); + createIssueIfNotFound("17", futurama, false, false); + createIssueIfNotFound("18", futurama, false, false); + createIssueIfNotFound("19", futurama, false, false); + createIssueIfNotFound("20", futurama, false, false); + createIssueIfNotFound("21", futurama, false, false); + createIssueIfNotFound("22", futurama, false, false); + createIssueIfNotFound("1", futuramaco2, false, false); + createIssueIfNotFound("2", futuramaco2, false, false); + createIssueIfNotFound("1", battlepope, false, false); + createIssueIfNotFound("2", battlepope, false, false); + createIssueIfNotFound("3", battlepope, false, false); + createIssueIfNotFound("11", bartsimpsontree, false, false); + createIssueIfNotFound("20", bartsimpson, false, false); + createIssueIfNotFound("21", bartsimpson, false, false); + createIssueIfNotFound("22", bartsimpson, false, false); + createIssueIfNotFound("23", bartsimpson, false, false); + createIssueIfNotFound("24", bartsimpson, false, false); + createIssueIfNotFound("25", bartsimpson, false, false); + createIssueIfNotFound("1", athena, false, false); + createIssueIfNotFound("1", athenamanhunter, false, false); + createIssueIfNotFound("2", athenamanhunter, false, false); + createIssueIfNotFound("3", athenamanhunter, false, false); + createIssueIfNotFound("4", athenamanhunter, false, false); + createIssueIfNotFound("5", athenamanhunter, false, false); + createIssueIfNotFound("6", athenamanhunter, false, false); + createIssueIfNotFound("1", blackwidow, false, false); + createIssueIfNotFound("2", blackwidow, false, false); + createIssueIfNotFound("3", blackwidow, false, false); + createIssueIfNotFound("4", blackwidow, false, false); + createIssueIfNotFound("5", blackwidow, false, false); + createIssueIfNotFound("6", blackwidow, false, false); + createIssueIfNotFound("1", blackwidow2, false, false); + createIssueIfNotFound("2", blackwidow2, false, false); + createIssueIfNotFound("3", blackwidow2, false, false); + createIssueIfNotFound("4", blackwidow2, false, false); + createIssueIfNotFound("5", blackwidow2, false, false); + createIssueIfNotFound("1", ruse, false, false); + createIssueIfNotFound("2", ruse, false, false); + createIssueIfNotFound("3", ruse, false, false); + createIssueIfNotFound("4", ruse, false, false); + createIssueIfNotFound("5", ruse, false, false); + createIssueIfNotFound("6", ruse, false, false); + createIssueIfNotFound("7", ruse, false, false); + createIssueIfNotFound("8", ruse, false, false); + createIssueIfNotFound("9", ruse, false, false); + createIssueIfNotFound("10", ruse, false, false); + createIssueIfNotFound("11", ruse, false, false); + createIssueIfNotFound("12", ruse, false, false); + createIssueIfNotFound("13", ruse, false, false); + createIssueIfNotFound("14", ruse, false, false); + createIssueIfNotFound("15", ruse, false, false); + createIssueIfNotFound("16", ruse, false, false); + createIssueIfNotFound("17", ruse, false, false); + createIssueIfNotFound("18", ruse, false, false); + createIssueIfNotFound("19", ruse, false, false); + createIssueIfNotFound("20", ruse, false, false); + createIssueIfNotFound("21", ruse, false, false); + createIssueIfNotFound("22", ruse, false, false); + createIssueIfNotFound("23", ruse, false, false); + createIssueIfNotFound("24", ruse, false, false); + createIssueIfNotFound("25", ruse, false, false); + createIssueIfNotFound("26", ruse, false, false); + createIssueIfNotFound("1", samurai, false, false); + createIssueIfNotFound("2", samurai, false, false); + createIssueIfNotFound("3", samurai, false, false); + createIssueIfNotFound("4", samurai, false, false); + createIssueIfNotFound("1", amazingfantasy, false, false); + createIssueIfNotFound("2", amazingfantasy, false, false); + createIssueIfNotFound("3", amazingfantasy, false, false); + createIssueIfNotFound("4", amazingfantasy, false, false); + createIssueIfNotFound("5", amazingfantasy, false, false); + createIssueIfNotFound("6", amazingfantasy, false, false); + createIssueIfNotFound("7", amazingfantasy, false, false); + createIssueIfNotFound("8", amazingfantasy, false, false); + createIssueIfNotFound("9", amazingfantasy, false, false); + createIssueIfNotFound("10", amazingfantasy, false, false); + createIssueIfNotFound("11", amazingfantasy, false, false); + createIssueIfNotFound("12", amazingfantasy, false, false); + createIssueIfNotFound("13", amazingfantasy, false, false); + createIssueIfNotFound("14", amazingfantasy, false, false); + createIssueIfNotFound("15", amazingfantasy, false, false); + createIssueIfNotFound("1", excalibur, false, false); + createIssueIfNotFound("2", excalibur, false, false); + createIssueIfNotFound("3", excalibur, false, false); + createIssueIfNotFound("4", excalibur, false, false); + createIssueIfNotFound("5", excalibur, false, false); + createIssueIfNotFound("6", excalibur, false, false); + createIssueIfNotFound("7", excalibur, false, false); + createIssueIfNotFound("8", excalibur, false, false); + createIssueIfNotFound("9", excalibur, false, false); + createIssueIfNotFound("10", excalibur, false, false); + createIssueIfNotFound("11", excalibur, false, false); + createIssueIfNotFound("12", excalibur, false, false); + createIssueIfNotFound("1", emmafrost, false, false); + createIssueIfNotFound("2", emmafrost, false, false); + createIssueIfNotFound("3", emmafrost, false, false); + createIssueIfNotFound("4", emmafrost, false, false); + createIssueIfNotFound("5", emmafrost, false, false); + createIssueIfNotFound("6", emmafrost, false, false); + createIssueIfNotFound("7", emmafrost, false, false); + createIssueIfNotFound("8", emmafrost, false, false); + createIssueIfNotFound("9", emmafrost, false, false); + createIssueIfNotFound("10", emmafrost, false, false); + createIssueIfNotFound("11", emmafrost, false, false); + createIssueIfNotFound("12", emmafrost, false, false); + createIssueIfNotFound("13", emmafrost, false, false); + createIssueIfNotFound("14", emmafrost, false, false); + createIssueIfNotFound("15", emmafrost, false, false); + createIssueIfNotFound("16", emmafrost, false, false); + createIssueIfNotFound("17", emmafrost, false, false); + createIssueIfNotFound("18", emmafrost, false, false); + createIssueIfNotFound("1", catwomanrome, false, false); + createIssueIfNotFound("2", catwomanrome, false, false); + createIssueIfNotFound("3", catwomanrome, false, false); + createIssueIfNotFound("4", catwomanrome, false, false); + createIssueIfNotFound("5", catwomanrome, false, false); + createIssueIfNotFound("6", catwomanrome, false, false); + createIssueIfNotFound("1", districtx, false, false); + createIssueIfNotFound("2", districtx, false, false); + createIssueIfNotFound("3", districtx, false, false); + createIssueIfNotFound("4", districtx, false, false); + createIssueIfNotFound("5", districtx, false, false); + createIssueIfNotFound("6", districtx, false, false); + createIssueIfNotFound("7", districtx, false, false); + createIssueIfNotFound("8", districtx, false, false); + createIssueIfNotFound("9", districtx, false, false); + createIssueIfNotFound("10", districtx, false, false); + createIssueIfNotFound("11", districtx, false, false); + createIssueIfNotFound("12", districtx, false, false); + createIssueIfNotFound("13", districtx, false, false); + createIssueIfNotFound("14", districtx, false, false); + createIssueIfNotFound("1", elcazador, false, false); + createIssueIfNotFound("2", elcazador, false, false); + createIssueIfNotFound("3", elcazador, false, false); + createIssueIfNotFound("4", elcazador, false, false); + createIssueIfNotFound("5", elcazador, false, false); + createIssueIfNotFound("6", elcazador, false, false); + createIssueIfNotFound("1", elcazadortom, false, false); + createIssueIfNotFound("1", elsinore, false, false); + createIssueIfNotFound("1", dreampolice, false, false); + createIssueIfNotFound("1", dragonlance, false, false); + createIssueIfNotFound("2", dragonlance, false, false); + createIssueIfNotFound("1", ghostrider, false, false); + createIssueIfNotFound("2", ghostrider, false, false); + createIssueIfNotFound("3", ghostrider, false, false); + createIssueIfNotFound("4", ghostrider, false, false); + createIssueIfNotFound("5", ghostrider, false, false); + createIssueIfNotFound("6", ghostrider, false, false); + createIssueIfNotFound("1", fathomkillian, false, false); + createIssueIfNotFound("2", fathomkillian, false, false); + createIssueIfNotFound("3", fathomkillian, false, false); + createIssueIfNotFound("4", fathomkillian, false, false); + createIssueIfNotFound("1", maryjane, false, false); + createIssueIfNotFound("2", maryjane, false, false); + createIssueIfNotFound("3", maryjane, false, false); + createIssueIfNotFound("4", maryjane, false, false); + createIssueIfNotFound("1", maryjanehome, false, false); + createIssueIfNotFound("2", maryjanehome, false, false); + createIssueIfNotFound("3", maryjanehome, false, false); + createIssueIfNotFound("4", maryjanehome, false, false); + createIssueIfNotFound("1", marville, false, false); + createIssueIfNotFound("2", marville, false, false); + createIssueIfNotFound("3", marville, false, false); + createIssueIfNotFound("4", marville, false, false); + createIssueIfNotFound("5", marville, false, false); + createIssueIfNotFound("6", marville, false, false); + createIssueIfNotFound("7", marville, false, false); + createIssueIfNotFound("1", megacity, false, false); + createIssueIfNotFound("2", megacity, false, false); + createIssueIfNotFound("3", megacity, false, false); + createIssueIfNotFound("4", megacity, false, false); + createIssueIfNotFound("5", megacity, false, false); + createIssueIfNotFound("6", megacity, false, false); + createIssueIfNotFound("7", megacity, false, false); + createIssueIfNotFound("8", megacity, false, false); + createIssueIfNotFound("1", nightcrawler, false, false); + createIssueIfNotFound("2", nightcrawler, false, false); + createIssueIfNotFound("3", nightcrawler, false, false); + createIssueIfNotFound("4", nightcrawler, false, false); + createIssueIfNotFound("5", nightcrawler, false, false); + createIssueIfNotFound("6", nightcrawler, false, false); + createIssueIfNotFound("7", nightcrawler, false, false); + createIssueIfNotFound("8", nightcrawler, false, false); + createIssueIfNotFound("9", nightcrawler, false, false); + createIssueIfNotFound("10", nightcrawler, false, false); + createIssueIfNotFound("11", nightcrawler, false, false); + createIssueIfNotFound("12", nightcrawler, false, false); + createIssueIfNotFound("1", ororo, false, false); + createIssueIfNotFound("1", radix, false, false); + createIssueIfNotFound("2", radix, false, false); + createIssueIfNotFound("3", radix, false, false); + createIssueIfNotFound("1", rogue, false, false); + createIssueIfNotFound("2", rogue, false, false); + createIssueIfNotFound("3", rogue, false, false); + createIssueIfNotFound("4", rogue, false, false); + createIssueIfNotFound("5", rogue, false, false); + createIssueIfNotFound("6", rogue, false, false); + createIssueIfNotFound("7", rogue, false, false); + createIssueIfNotFound("8", rogue, false, false); + createIssueIfNotFound("9", rogue, false, false); + createIssueIfNotFound("10", rogue, false, false); + createIssueIfNotFound("11", rogue, false, false); + createIssueIfNotFound("12", rogue, false, false); + createIssueIfNotFound("1", shijunen, false, false); + createIssueIfNotFound("2", shijunen, false, false); + createIssueIfNotFound("3", shijunen, false, false); + createIssueIfNotFound("4", shijunen, false, false); + createIssueIfNotFound("1", solus, false, false); + createIssueIfNotFound("2", solus, false, false); + createIssueIfNotFound("3", solus, false, false); + createIssueIfNotFound("4", solus, false, false); + createIssueIfNotFound("5", solus, false, false); + createIssueIfNotFound("6", solus, false, false); + createIssueIfNotFound("7", solus, false, false); + createIssueIfNotFound("8", solus, false, false); + createIssueIfNotFound("1", toxin, false, false); + createIssueIfNotFound("2", toxin, false, false); + createIssueIfNotFound("3", toxin, false, false); + createIssueIfNotFound("4", toxin, false, false); + createIssueIfNotFound("5", toxin, false, false); + createIssueIfNotFound("6", toxin, false, false); + createIssueIfNotFound("1", wildgirl, false, false); + createIssueIfNotFound("2", wildgirl, false, false); + createIssueIfNotFound("3", wildgirl, false, false); + createIssueIfNotFound("4", wildgirl, false, false); + createIssueIfNotFound("5", wildgirl, false, false); + createIssueIfNotFound("6", wildgirl, false, false); + createIssueIfNotFound("1", wildcats, false, false); + createIssueIfNotFound("1", wildsiderz, false, false); + createIssueIfNotFound("19", vampirella, false, false); + createIssueIfNotFound("1", wolverine, false, false); + createIssueIfNotFound("2", wolverine, false, false); + createIssueIfNotFound("3", wolverine, false, false); + createIssueIfNotFound("4", wolverine, false, false); + createIssueIfNotFound("5", wolverine, false, false); + createIssueIfNotFound("6", wolverine, false, false); + createIssueIfNotFound("1", woodboy, false, false); + createIssueIfNotFound("1", wraithborn, false, false); + createIssueIfNotFound("2", wraithborn, false, false); + createIssueIfNotFound("3", wraithborn, false, false); + createIssueIfNotFound("1", x23, false, false); + createIssueIfNotFound("2", x23, false, false); + createIssueIfNotFound("3", x23, false, false); + createIssueIfNotFound("4", x23, false, false); + createIssueIfNotFound("5", x23, false, false); + createIssueIfNotFound("6", x23, false, false); + createIssueIfNotFound("46", xtremexmen, false, false); + createIssueIfNotFound("1", xmenkittypryde, false, false); + createIssueIfNotFound("2", xmenkittypryde, false, false); + createIssueIfNotFound("3", xmenkittypryde, false, false); + createIssueIfNotFound("4", xmenkittypryde, false, false); + createIssueIfNotFound("5", xmenkittypryde, false, false); + createIssueIfNotFound("1", witchbladetombraider, false, false); + createIssueIfNotFound("1", xmenageofapocalypseoneshot, false, false); + createIssueIfNotFound("1", xmenageofapocalypse, false, false); + createIssueIfNotFound("2", xmenageofapocalypse, false, false); + createIssueIfNotFound("3", xmenageofapocalypse, false, false); + createIssueIfNotFound("4", xmenageofapocalypse, false, false); + createIssueIfNotFound("5", xmenageofapocalypse, false, false); + createIssueIfNotFound("6", xmenageofapocalypse, false, false); + createIssueIfNotFound("1", thorsonasgard, false, false); + createIssueIfNotFound("2", thorsonasgard, false, false); + createIssueIfNotFound("3", thorsonasgard, false, false); + createIssueIfNotFound("4", thorsonasgard, false, false); + createIssueIfNotFound("5", thorsonasgard, false, false); + createIssueIfNotFound("6", thorsonasgard, false, false); + createIssueIfNotFound("7", thorsonasgard, false, false); + createIssueIfNotFound("8", thorsonasgard, false, false); + createIssueIfNotFound("9", thorsonasgard, false, false); + createIssueIfNotFound("10", thorsonasgard, false, false); + createIssueIfNotFound("11", thorsonasgard, false, false); + createIssueIfNotFound("12", thorsonasgard, false, false); + createIssueIfNotFound("1", strange, false, false); + createIssueIfNotFound("2", strange, false, false); + createIssueIfNotFound("3", strange, false, false); + createIssueIfNotFound("4", strange, false, false); + createIssueIfNotFound("5", strange, false, false); + createIssueIfNotFound("6", strange, false, false); + createIssueIfNotFound("0", supergirl, false, false); + createIssueIfNotFound("1", supergirl, false, false); + createIssueIfNotFound("2", supergirl, false, false); + createIssueIfNotFound("3", supergirl, false, false); + createIssueIfNotFound("4", supergirl, false, false); + createIssueIfNotFound("1", robertjordanwheeloftime, false, false); + createIssueIfNotFound("2", robertjordanwheeloftime, false, false); + createIssueIfNotFound("1", stardustkid, false, false); + createIssueIfNotFound("2", stardustkid, false, false); + createIssueIfNotFound("3", stardustkid, false, false); + createIssueIfNotFound("207", superman, false, false); + createIssueIfNotFound("208", superman, false, false); + createIssueIfNotFound("209", superman, false, false); + createIssueIfNotFound("210", superman, false, false); + createIssueIfNotFound("211", superman, false, false); + createIssueIfNotFound("212", superman, false, false); + createIssueIfNotFound("213", superman, false, false); + createIssueIfNotFound("214", superman, false, false); + createIssueIfNotFound("215", superman, false, false); + createIssueIfNotFound("17", supermanbatman, false, false); + createIssueIfNotFound("18", supermanbatman, false, false); + createIssueIfNotFound("19", supermanbatman, false, false); + createIssueIfNotFound("20", supermanbatman, false, false); + createIssueIfNotFound("21", supermanbatman, false, false); + createIssueIfNotFound("22", supermanbatman, false, false); + createIssueIfNotFound("1", tombraidergreatesttreasure, false, false); + createIssueIfNotFound("48", tombraider, false, false); + createIssueIfNotFound("49", tombraider, false, false); + createIssueIfNotFound("50", tombraider, false, false); + createIssueIfNotFound("1", tomstrong, false, false); + createIssueIfNotFound("1", tombdracula, false, false); + createIssueIfNotFound("1", thetenth, false, false); + createIssueIfNotFound("1", devilskeeper, false, false); + createIssueIfNotFound("1", artgreghorn, false, false); + createIssueIfNotFound("81", witchblade, false, false); + createIssueIfNotFound("82", witchblade, false, false); + createIssueIfNotFound("83", witchblade, false, false); + createIssueIfNotFound("84", witchblade, false, false); + createIssueIfNotFound("85", witchblade, false, false); + createIssueIfNotFound("86", witchblade, false, false); + createIssueIfNotFound("87", witchblade, false, false); + createIssueIfNotFound("88", witchblade, false, false); + createIssueIfNotFound("89", witchblade, false, false); + createIssueIfNotFound("90", witchblade, false, false); + createIssueIfNotFound("91", witchblade, false, false); + createIssueIfNotFound("92", witchblade, false, false); + createIssueIfNotFound("19", tarotblackrose, false, false); + createIssueIfNotFound("20", tarotblackrose, false, false); + createIssueIfNotFound("21", tarotblackrose, false, false); + createIssueIfNotFound("22", tarotblackrose, false, false); + createIssueIfNotFound("23", tarotblackrose, false, false); + createIssueIfNotFound("24", tarotblackrose, false, false); + createIssueIfNotFound("25", tarotblackrose, false, false); + createIssueIfNotFound("26", tarotblackrose, false, false); + createIssueIfNotFound("27", tarotblackrose, false, false); + createIssueIfNotFound("28", tarotblackrose, false, false); + createIssueIfNotFound("29", tarotblackrose, false, false); + createIssueIfNotFound("30", tarotblackrose, false, false); + createIssueIfNotFound("31", tarotblackrose, false, false); + createIssueIfNotFound("32", tarotblackrose, false, false); + createIssueIfNotFound("33", tarotblackrose, false, false); + createIssueIfNotFound("34", tarotblackrose, false, false); + createIssueIfNotFound("35", tarotblackrose, false, false); + createIssueIfNotFound("1", spectacularspiderman, false, false); + createIssueIfNotFound("2", spectacularspiderman, false, false); + createIssueIfNotFound("3", spectacularspiderman, false, false); + createIssueIfNotFound("4", spectacularspiderman, false, false); + createIssueIfNotFound("5", spectacularspiderman, false, false); + createIssueIfNotFound("6", spectacularspiderman, false, false); + createIssueIfNotFound("7", spectacularspiderman, false, false); + createIssueIfNotFound("8", spectacularspiderman, false, false); + createIssueIfNotFound("9", spectacularspiderman, false, false); + createIssueIfNotFound("10", spectacularspiderman, false, false); + createIssueIfNotFound("11", spectacularspiderman, false, false); + createIssueIfNotFound("12", spectacularspiderman, false, false); + createIssueIfNotFound("13", spectacularspiderman, false, false); + createIssueIfNotFound("14", spectacularspiderman, false, false); + createIssueIfNotFound("15", spectacularspiderman, false, false); + createIssueIfNotFound("16", spectacularspiderman, false, false); + createIssueIfNotFound("17", spectacularspiderman, false, false); + createIssueIfNotFound("18", spectacularspiderman, false, false); + createIssueIfNotFound("19", spectacularspiderman, false, false); + createIssueIfNotFound("20", spectacularspiderman, false, false); + createIssueIfNotFound("21", spectacularspiderman, false, false); + createIssueIfNotFound("22", spectacularspiderman, false, false); + createIssueIfNotFound("23", spectacularspiderman, false, false); + createIssueIfNotFound("24", spectacularspiderman, false, false); + createIssueIfNotFound("25", spectacularspiderman, false, false); + createIssueIfNotFound("26", spectacularspiderman, false, false); + createIssueIfNotFound("1", soulfire, false, false); + createIssueIfNotFound("2", soulfire, false, false); + createIssueIfNotFound("3", soulfire, false, false); + createIssueIfNotFound("4", soulfire, false, false); + createIssueIfNotFound("5", soulfire, false, false); + createIssueIfNotFound("1", soulfirelight, false, false); + createIssueIfNotFound("2", soulfirelight, false, false); + createIssueIfNotFound("3", soulfirelight, false, false); + createIssueIfNotFound("4", soulfirelight, false, false); + createIssueIfNotFound("1", spellbinders, false, false); + createIssueIfNotFound("2", spellbinders, false, false); + createIssueIfNotFound("3", spellbinders, false, false); + createIssueIfNotFound("4", spellbinders, false, false); + createIssueIfNotFound("5", spellbinders, false, false); + createIssueIfNotFound("6", spellbinders, false, false); + createIssueIfNotFound("1", spidermanlovesmary, false, false); + createIssueIfNotFound("1", spidermanindia, false, false); + createIssueIfNotFound("2", spidermanindia, false, false); + createIssueIfNotFound("3", spidermanindia, false, false); + createIssueIfNotFound("4", spidermanindia, false, false); + createIssueIfNotFound("1", spidermanteam, false, false); + createIssueIfNotFound("2", spidermanteam, false, false); + createIssueIfNotFound("3", spidermanteam, false, false); + createIssueIfNotFound("4", spidermanteam, false, false); + createIssueIfNotFound("5", spidermanteam, false, false); + createIssueIfNotFound("1", spidermanbreakout, false, false); + createIssueIfNotFound("2", spidermanbreakout, false, false); + createIssueIfNotFound("3", spidermanbreakout, false, false); + createIssueIfNotFound("4", spidermanbreakout, false, false); + createIssueIfNotFound("5", spidermanbreakout, false, false); + createIssueIfNotFound("1", spidermanhousem, false, false); + createIssueIfNotFound("2", spidermanhousem, false, false); + createIssueIfNotFound("3", spidermanhousem, false, false); + createIssueIfNotFound("4", spidermanhousem, false, false); + createIssueIfNotFound("13", sojourn, false, false); + createIssueIfNotFound("14", sojourn, false, false); + createIssueIfNotFound("15", sojourn, false, false); + createIssueIfNotFound("16", sojourn, false, false); + createIssueIfNotFound("17", sojourn, false, false); + createIssueIfNotFound("18", sojourn, false, false); + createIssueIfNotFound("19", sojourn, false, false); + createIssueIfNotFound("20", sojourn, false, false); + createIssueIfNotFound("21", sojourn, false, false); + createIssueIfNotFound("22", sojourn, false, false); + createIssueIfNotFound("23", sojourn, false, false); + createIssueIfNotFound("24", sojourn, false, false); + createIssueIfNotFound("25", sojourn, false, false); + createIssueIfNotFound("26", sojourn, false, false); + createIssueIfNotFound("27", sojourn, false, false); + createIssueIfNotFound("28", sojourn, false, false); + createIssueIfNotFound("29", sojourn, false, false); + createIssueIfNotFound("30", sojourn, false, false); + createIssueIfNotFound("31", sojourn, false, false); + createIssueIfNotFound("32", sojourn, false, false); + createIssueIfNotFound("33", sojourn, false, false); + createIssueIfNotFound("34", sojourn, false, false); + createIssueIfNotFound("1", shrek, false, false); + createIssueIfNotFound("2", shrek, false, false); + createIssueIfNotFound("1", shanna, false, false); + createIssueIfNotFound("2", shanna, false, false); + createIssueIfNotFound("3", shanna, false, false); + createIssueIfNotFound("4", shanna, false, false); + createIssueIfNotFound("5", shanna, false, false); + createIssueIfNotFound("6", shanna, false, false); + createIssueIfNotFound("7", shanna, false, false); + createIssueIfNotFound("100", simpsons, false, false); + createIssueIfNotFound("101", simpsons, false, false); + createIssueIfNotFound("102", simpsons, false, false); + createIssueIfNotFound("103", simpsons, false, false); + createIssueIfNotFound("104", simpsons, false, false); + createIssueIfNotFound("105", simpsons, false, false); + createIssueIfNotFound("106", simpsons, false, false); + createIssueIfNotFound("107", simpsons, false, false); + createIssueIfNotFound("108", simpsons, false, false); + createIssueIfNotFound("109", simpsons, false, false); + createIssueIfNotFound("110", simpsons, false, false); + createIssueIfNotFound("111", simpsons, false, false); + createIssueIfNotFound("112", simpsons, false, false); + createIssueIfNotFound("1", newxmenhellions, false, false); + createIssueIfNotFound("2", newxmenhellions, false, false); + createIssueIfNotFound("3", newxmenhellions, false, false); + createIssueIfNotFound("4", newxmenhellions, false, false); + createIssueIfNotFound("1", newxmenacademy, false, false); + createIssueIfNotFound("2", newxmenacademy, false, false); + createIssueIfNotFound("3", newxmenacademy, false, false); + createIssueIfNotFound("4", newxmenacademy, false, false); + createIssueIfNotFound("5", newxmenacademy, false, false); + createIssueIfNotFound("6", newxmenacademy, false, false); + createIssueIfNotFound("7", newxmenacademy, false, false); + createIssueIfNotFound("8", newxmenacademy, false, false); + createIssueIfNotFound("9", newxmenacademy, false, false); + createIssueIfNotFound("10", newxmenacademy, false, false); + createIssueIfNotFound("11", newxmenacademy, false, false); + createIssueIfNotFound("12", newxmenacademy, false, false); + createIssueIfNotFound("13", newxmenacademy, false, false); + createIssueIfNotFound("14", newxmenacademy, false, false); + createIssueIfNotFound("15", newxmenacademy, false, false); + createIssueIfNotFound("16", newxmenacademy, false, false); + createIssueIfNotFound("151", newxmen, false, false); + createIssueIfNotFound("152", newxmen, false, false); + createIssueIfNotFound("153", newxmen, false, false); + createIssueIfNotFound("154", newxmen, false, false); + createIssueIfNotFound("1", redsonja, false, false); + createIssueIfNotFound("2", redsonja, false, false); + createIssueIfNotFound("3", redsonja, false, false); + createIssueIfNotFound("1", revelations, false, false); + createIssueIfNotFound("2", revelations, false, false); + createIssueIfNotFound("3", revelations, false, false); + createIssueIfNotFound("4", revelations, false, false); + createIssueIfNotFound("1", kisskissbangbang, false, false); + createIssueIfNotFound("2", kisskissbangbang, false, false); + createIssueIfNotFound("3", kisskissbangbang, false, false); + createIssueIfNotFound("4", kisskissbangbang, false, false); + createIssueIfNotFound("5", kisskissbangbang, false, false); + createIssueIfNotFound("1", loki, false, false); + createIssueIfNotFound("2", loki, false, false); + createIssueIfNotFound("3", loki, false, false); + createIssueIfNotFound("4", loki, false, false); + createIssueIfNotFound("1", lullaby, false, false); + createIssueIfNotFound("2", lullaby, false, false); + createIssueIfNotFound("3", lullaby, false, false); + createIssueIfNotFound("1", legendofisis, false, false); + createIssueIfNotFound("2", legendofisis, false, false); + createIssueIfNotFound("1", judge, false, false); + createIssueIfNotFound("2", judge, false, false); + createIssueIfNotFound("3", judge, false, false); + createIssueIfNotFound("1", friendlyneighborspider, false, false); + createIssueIfNotFound("2", friendlyneighborspider, false, false); + createIssueIfNotFound("3", friendlyneighborspider, false, false); + createIssueIfNotFound("1", gift, false, false); + createIssueIfNotFound("2", gift, false, false); + createIssueIfNotFound("3", gift, false, false); + createIssueIfNotFound("4", gift, false, false); + createIssueIfNotFound("5", gift, false, false); + createIssueIfNotFound("6", gift, false, false); + createIssueIfNotFound("7", gift, false, false); + createIssueIfNotFound("8", gift, false, false); + createIssueIfNotFound("9", gift, false, false); + createIssueIfNotFound("10", gift, false, false); + createIssueIfNotFound("11", gift, false, false); + createIssueIfNotFound("12", gift, false, false); + createIssueIfNotFound("13", gift, false, false); + createIssueIfNotFound("1/2", fathom, false, false); + createIssueIfNotFound("1", fathom, false, false); + createIssueIfNotFound("2", fathom, false, false); + createIssueIfNotFound("3", fathom, false, false); + createIssueIfNotFound("4", fathom, false, false); + createIssueIfNotFound("5", fathom, false, false); + createIssueIfNotFound("6", fathom, false, false); + createIssueIfNotFound("7", fathom, false, false); + createIssueIfNotFound("8", fathom, false, false); + createIssueIfNotFound("9", fathom, false, false); + createIssueIfNotFound("10", fathom, false, false); + createIssueIfNotFound("11", fathom, false, false); + createIssueIfNotFound("12", fathom, false, false); + createIssueIfNotFound("13", fathom, false, false); + createIssueIfNotFound("14", fathom, false, false); + createIssueIfNotFound("1", fathombeginnings, false, false); + createIssueIfNotFound("1", fathomcannon, false, false); + createIssueIfNotFound("2", fathomcannon, false, false); + createIssueIfNotFound("3", fathomcannon, false, false); + createIssueIfNotFound("1", fathomcannonprelude, false, false); + createIssueIfNotFound("1", fathomdawnofwar, false, false); + createIssueIfNotFound("2", fathomdawnofwar, false, false); + createIssueIfNotFound("3", fathomdawnofwar, false, false); + createIssueIfNotFound("1", fathomprelude, false, false); + createIssueIfNotFound("1", fathomswimsuit, false, false); + createIssueIfNotFound("1", fathomswimsuit2000, false, false); + createIssueIfNotFound("0", fathomvol2, false, false); + createIssueIfNotFound("1", fathomvol2, false, false); + createIssueIfNotFound("2", fathomvol2, false, false); + createIssueIfNotFound("3", fathomvol2, false, false); + createIssueIfNotFound("4", fathomvol2, false, false); + createIssueIfNotFound("5", fathomvol2, false, false); + createIssueIfNotFound("1", flakriot, false, false); + createIssueIfNotFound("1", freshmen, false, false); + createIssueIfNotFound("1", daringescapes, false, false); + createIssueIfNotFound("2", daringescapes, false, false); + createIssueIfNotFound("3", daringescapes, false, false); + createIssueIfNotFound("4", daringescapes, false, false); + createIssueIfNotFound("1", brath, false, false); + createIssueIfNotFound("2", brath, false, false); + createIssueIfNotFound("3", brath, false, false); + createIssueIfNotFound("4", brath, false, false); + createIssueIfNotFound("5", brath, false, false); + createIssueIfNotFound("6", brath, false, false); + createIssueIfNotFound("7", brath, false, false); + createIssueIfNotFound("8", brath, false, false); + createIssueIfNotFound("9", brath, false, false); + createIssueIfNotFound("10", brath, false, false); + createIssueIfNotFound("11", brath, false, false); + createIssueIfNotFound("12", brath, false, false); + createIssueIfNotFound("13", brath, false, false); + createIssueIfNotFound("14", brath, false, false); + createIssueIfNotFound("1", astonishingxmen, false, false); + createIssueIfNotFound("2", astonishingxmen, false, false); + createIssueIfNotFound("3", astonishingxmen, false, false); + createIssueIfNotFound("4", astonishingxmen, false, false); + createIssueIfNotFound("5", astonishingxmen, false, false); + createIssueIfNotFound("6", astonishingxmen, false, false); + createIssueIfNotFound("7", astonishingxmen, false, false); + createIssueIfNotFound("8", astonishingxmen, false, false); + createIssueIfNotFound("9", astonishingxmen, false, false); + createIssueIfNotFound("10", astonishingxmen, false, false); + createIssueIfNotFound("11", astonishingxmen, false, false); + createIssueIfNotFound("12", astonishingxmen, false, false); + createIssueIfNotFound("1", barbarossa, false, false); + createIssueIfNotFound("1", aspenComic, false, false); + createIssueIfNotFound("2", aspenComic, false, false); + createIssueIfNotFound("3", aspenComic, false, false); + createIssueIfNotFound("1", armydarknessreanim, false, false); + createIssueIfNotFound("2", armydarknessreanim, false, false); + createIssueIfNotFound("3", armydarknessreanim, false, false); + createIssueIfNotFound("1", hellcop, false, false); + createIssueIfNotFound("2", hellcop, false, false); + createIssueIfNotFound("3", hellcop, false, false); + createIssueIfNotFound("4", hellcop, false, false); + createIssueIfNotFound("1", housem, false, false); + createIssueIfNotFound("2", housem, false, false); + createIssueIfNotFound("3", housem, false, false); + createIssueIfNotFound("0", hunterkiller, false, false); + createIssueIfNotFound("1", hunterkiller, false, false); + createIssueIfNotFound("2", hunterkiller, false, false); + createIssueIfNotFound("3", hunterkiller, false, false); + createIssueIfNotFound("4", hunterkiller, false, false); + createIssueIfNotFound("1", hunterkillerdossier, false, false); + createIssueIfNotFound("1", ironghost, false, false); + createIssueIfNotFound("1", magdalenavampirella2, false, false); + createIssueIfNotFound("20", marvelknightspider, false, false); + createIssueIfNotFound("39", meridian, false, false); + createIssueIfNotFound("40", meridian, false, false); + createIssueIfNotFound("41", meridian, false, false); + createIssueIfNotFound("42", meridian, false, false); + createIssueIfNotFound("43", meridian, false, false); + createIssueIfNotFound("44", meridian, false, false); + createIssueIfNotFound("1", necromancer, false, false); + createIssueIfNotFound("2", necromancer, false, false); + createIssueIfNotFound("3", necromancer, false, false); + createIssueIfNotFound("1", negationwar, false, false); + createIssueIfNotFound("2", negationwar, false, false); + createIssueIfNotFound("1", mystique, false, false); + createIssueIfNotFound("2", mystique, false, false); + createIssueIfNotFound("3", mystique, false, false); + createIssueIfNotFound("4", mystique, false, false); + createIssueIfNotFound("5", mystique, false, false); + createIssueIfNotFound("6", mystique, false, false); + createIssueIfNotFound("7", mystique, false, false); + createIssueIfNotFound("8", mystique, false, false); + createIssueIfNotFound("9", mystique, false, false); + createIssueIfNotFound("10", mystique, false, false); + createIssueIfNotFound("11", mystique, false, false); + createIssueIfNotFound("12", mystique, false, false); + createIssueIfNotFound("13", mystique, false, false); + createIssueIfNotFound("14", mystique, false, false); + createIssueIfNotFound("15", mystique, false, false); + createIssueIfNotFound("16", mystique, false, false); + createIssueIfNotFound("17", mystique, false, false); + createIssueIfNotFound("18", mystique, false, false); + createIssueIfNotFound("19", mystique, false, false); + createIssueIfNotFound("20", mystique, false, false); + createIssueIfNotFound("21", mystique, false, false); + createIssueIfNotFound("22", mystique, false, false); + createIssueIfNotFound("23", mystique, false, false); + createIssueIfNotFound("24", mystique, false, false); + createIssueIfNotFound("1", abadazad, false, false); + createIssueIfNotFound("2", abadazad, false, false); + createIssueIfNotFound("3", abadazad, false, false); + createIssueIfNotFound("503", amazingspiderman, false, false); + createIssueIfNotFound("504", amazingspiderman, false, false); + createIssueIfNotFound("505", amazingspiderman, false, false); + createIssueIfNotFound("506", amazingspiderman, false, false); + createIssueIfNotFound("507", amazingspiderman, false, false); + createIssueIfNotFound("508", amazingspiderman, false, false); + createIssueIfNotFound("509", amazingspiderman, false, false); + createIssueIfNotFound("510", amazingspiderman, false, false); + createIssueIfNotFound("511", amazingspiderman, false, false); + createIssueIfNotFound("512", amazingspiderman, false, false); + createIssueIfNotFound("513", amazingspiderman, false, false); + createIssueIfNotFound("514", amazingspiderman, false, false); + createIssueIfNotFound("515", amazingspiderman, false, false); + createIssueIfNotFound("516", amazingspiderman, false, false); + createIssueIfNotFound("517", amazingspiderman, false, false); + createIssueIfNotFound("518", amazingspiderman, false, false); + createIssueIfNotFound("519", amazingspiderman, false, false); + createIssueIfNotFound("520", amazingspiderman, false, false); + createIssueIfNotFound("521", amazingspiderman, false, false); + createIssueIfNotFound("522", amazingspiderman, false, false); + createIssueIfNotFound("523", amazingspiderman, false, false); + createIssueIfNotFound("524", amazingspiderman, false, false); + createIssueIfNotFound("525", amazingspiderman, false, false); + createIssueIfNotFound("526", amazingspiderman, false, false); + createIssueIfNotFound("1", dangergirlbackinblack, false, false); + createIssueIfNotFound("2", dangergirlbackinblack, false, false); + createIssueIfNotFound("1", darknesssuperman, false, false); + createIssueIfNotFound("2", darknesssuperman, false, false); + createIssueIfNotFound("1", darknesstombraider, false, false); + createIssueIfNotFound("1", darknessvampirella, false, false); + createIssueIfNotFound("1", darknessblacksails, false, false); + createIssueIfNotFound("17", darknessvol2, false, false); + createIssueIfNotFound("18", darknessvol2, false, false); + createIssueIfNotFound("19", darknessvol2, false, false); + createIssueIfNotFound("20", darknessvol2, false, false); + createIssueIfNotFound("21", darknessvol2, false, false); + createIssueIfNotFound("22", darknessvol2, false, false); + createIssueIfNotFound("23", darknessvol2, false, false); + createIssueIfNotFound("1", aria, false, false); + createIssueIfNotFound("2", aria, false, false); + createIssueIfNotFound("3", aria, false, false); + createIssueIfNotFound("4", aria, false, false); + createIssueIfNotFound("1", ariasoulmarket, false, false); + createIssueIfNotFound("2", ariasoulmarket, false, false); + createIssueIfNotFound("3", ariasoulmarket, false, false); + createIssueIfNotFound("4", ariasoulmarket, false, false); + createIssueIfNotFound("5", ariasoulmarket, false, false); + createIssueIfNotFound("6", ariasoulmarket, false, false); + createIssueIfNotFound("1", ariasummerspell, false, false); + createIssueIfNotFound("2", ariasummerspell, false, false); + createIssueIfNotFound("1", ariaenchantment, false, false); + createIssueIfNotFound("2", ariaenchantment, false, false); + createIssueIfNotFound("3", ariaenchantment, false, false); + createIssueIfNotFound("4", ariaenchantment, false, false); + createIssueIfNotFound("1", armydarknessashes, false, false); + createIssueIfNotFound("2", armydarknessashes, false, false); + createIssueIfNotFound("3", armydarknessashes, false, false); + createIssueIfNotFound("4", armydarknessashes, false, false); + createIssueIfNotFound("1", armydarknessshop, false, false); + createIssueIfNotFound("2", armydarknessshop, false, false); + createIssueIfNotFound("1", hackslashtoys, false, false); + createIssueIfNotFound("2", hackslashtoys, false, false); + createIssueIfNotFound("1", harryjohnson, false, false); + createIssueIfNotFound("4", battlepope, false, false); + createIssueIfNotFound("5", battlepope, false, false); + createIssueIfNotFound("6", battlepope, false, false); + createIssueIfNotFound("7", battlepope, false, false); + createIssueIfNotFound("8", battlepope, false, false); + createIssueIfNotFound("9", battlepope, false, false); + createIssueIfNotFound("10", battlepope, false, false); + createIssueIfNotFound("11", battlepope, false, false); + createIssueIfNotFound("12", battlepope, false, false); + List volumes = volumeRepository.findAll(); + volumes.forEach(volume -> volumeRepository.delete(volume)); + moduleService.setDataImported(ComicConstants.COMICS); + } + + private Artist createArtistIfNotFound(String artistName) { + log.info("createArtistIfNotFound {}", artistName); + Artist artist = artistRepository.findByName(artistName); + if (artist == null) { + log.info("Artist {} not found, will create it", artistName); + artist = new Artist(); + artist.setName(artistName); + artistRepository.save(artist); + } + return artist; + } + + private Publisher createPublisherIfNotFound(String publisherName) { + log.info("createPublisherIfNotFound {}", publisherName); + Publisher publisher = publisherRepository.findByName(publisherName); + if (publisher == null) { + log.info("Publisher {} not found, will create it", publisherName); + publisher = new Publisher(); + publisher.setName(publisherName); + publisherRepository.save(publisher); + } + return publisher; + } + + private Worktype createWorktypeIfNotFound(String workTypeName) { + log.info("createWorktypeIfNotFound {}", workTypeName); + Worktype worktype = worktypeRepository.findByName(workTypeName); + if (worktype == null) { + log.info("Worktype {} not found, will create it", workTypeName); + worktype = new Worktype(); + worktype.setName(workTypeName); + worktypeRepository.save(worktype); + } + return worktype; + } + + private Comic createComicIfNotFound(Publisher publisher, String title, boolean currentOrder, boolean completed) { + log.info("createComicIfNotFound {} {} {} {}", publisher, title, currentOrder, completed); + Comic comic = comicRepository.findByTitleAndPublisher(title, publisher); + if (comic == null) { + log.info("Comic {} from {} not found, will create it", title, publisher.getName()); + comic = new Comic(); + comic.setTitle(title); + comic.setPublisher(publisher); + comic.setCurrentOrder(currentOrder); + comic.setCompleted(completed); + comicRepository.save(comic); + } + return comic; + } + + private ComicWork createComicWorkIfNotFound(Comic comic, Artist artist, Worktype worktype) { + log.info("createComicWorkIfNotFound {} {} {}", comic, artist, worktype); + ComicWork comicWork = comicWorkRepository.findbyComicAndArtistAndWorktype(comic, artist, worktype); + if (comicWork == null) { + log.info("ComicWork {} from {} for {} not found, will create it", worktype, artist, comic); + comicWork = new ComicWork(); + comicWork.setComic(comic); + comicWork.setArtist(artist); + comicWork.setWorkType(worktype); + comicWorkRepository.save(comicWork); + } + return comicWork; + } + + private void createStoryArcIfNotFound(String name, Comic comic) { + log.info("createStoryArcIfNotFound {} {}", comic, name); + StoryArc storyArc = storyArcRepository.findByNameAndComic(name, comic); + if (storyArc == null) { + log.info("StoryArc {} for {} not found, will create it", name, comic); + storyArc = new StoryArc(); + storyArc.setName(name); + storyArc.setComic(comic); + storyArcRepository.save(storyArc); + } + } + + private void createTradePaperbackIfNotFound(String name, Comic comic, int start, int end) { + log.info("createTradePaperbackIfNotFound {} {} {}-{}", comic, name, start, end); + TradePaperback tradePaperback = tradePaperbackRepository.findByFields(name, comic, start, end); + if (tradePaperback == null) { + log.info("TradePaperback {} for {} with issues {}-{} not found, will create it", name, comic, start, end); + tradePaperback = new TradePaperback(); + tradePaperback.setName(name); + tradePaperback.setComic(comic); + tradePaperback.setIssueStart(start); + tradePaperback.setIssueEnd(end); + tradePaperbackRepository.save(tradePaperback); + } + } + + private void createIssueIfNotFound(String issueNumber, Comic comic, boolean isRead, boolean inStock) { + log.info("createIssueIfNotFound {} {} {} {}", comic, issueNumber, isRead, inStock); + Issue issue = issueRepository.findByComicAndIssueNumber(comic, issueNumber); + if (issue == null) { + log.info("Issue {} for {} not found, will create it", issueNumber, comic); + issue = new Issue(); + issue.setIssueNumber(issueNumber); + issue.setComic(comic); + issue.setIsRead(isRead); + issue.setInStock(inStock); + issueRepository.save(issue); + } + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/Artist.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Artist.java new file mode 100644 index 0000000..6a3159d --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Artist.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import io.micrometer.common.lang.Nullable; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotEmpty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +/** + * Represents an artist in the system. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Slf4j +@Entity +public class Artist extends AbstractEntity { + + @NotEmpty + @Column(unique = true) + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "artist", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + List comicWorks; + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("Artist{"); + sb.append("name='").append(name).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/ArtistRepository.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/ArtistRepository.java new file mode 100644 index 0000000..a5b94f2 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/ArtistRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface ArtistRepository extends JpaRepository { + @Query("select a from Artist a " + + "where lower(a.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + List findByNameIgnoreCase(String name); + + Artist findByName(String name); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/Comic.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Comic.java new file mode 100644 index 0000000..bad2914 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Comic.java @@ -0,0 +1,77 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.LinkedList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import io.micrometer.common.lang.Nullable; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Represents a comic entity. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +public class Comic extends AbstractEntity { + + @NotEmpty + @Column(unique = true) + private String title; + + @ManyToOne + @JoinColumn(name = "publisher_id") + @NotNull + @JsonIgnoreProperties({ "comics" }) + private Publisher publisher; + + private Boolean currentOrder = false; + + private Boolean completed = false; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + List comicWorks; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + private List issues = new LinkedList<>(); + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + private List storyArcs = new LinkedList<>(); + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + private List tradePaperbacks = new LinkedList<>(); + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + private List volumes = new LinkedList<>(); + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("Comic{"); + sb.append("title='").append(title).append('\''); + sb.append(", publisher=").append(publisher); + sb.append(", currentOrder=").append(currentOrder); + sb.append(", completed=").append(completed); + sb.append('}'); + return sb.toString(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicRepository.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicRepository.java new file mode 100644 index 0000000..3a3e61f --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicRepository.java @@ -0,0 +1,19 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface ComicRepository extends JpaRepository { + @Query("select c from Comic c " + + "where lower(c.title) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + Comic findByTitleAndPublisher(String title, Publisher publisher); + + List findByTitle(String title); + + List findByTitleIgnoreCase(String title); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicWork.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicWork.java new file mode 100644 index 0000000..840f93c --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicWork.java @@ -0,0 +1,48 @@ +package de.thpeetz.kontor.comics.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +@Table(indexes = {@Index(columnList = "comic_id, artist_id, workType_id") }, + uniqueConstraints = @UniqueConstraint(columnNames = {"comic_id", "artist_id", "workType_id" }) +) +public class ComicWork extends AbstractEntity { + + @ManyToOne + @JoinColumn(name = "comic_id") + @NotNull + private Comic comic; + + @ManyToOne + @JoinColumn(name = "artist_id") + @NotNull + private Artist artist; + + @ManyToOne + @JoinColumn(name = "workType_id") + @NotNull + private Worktype workType; + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("ComicWork{"); + sb.append("comic=").append(comic); + sb.append(", artist=").append(artist); + sb.append(", workType=").append(workType); + sb.append('}'); + return sb.toString(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicWorkRepository.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicWorkRepository.java new file mode 100644 index 0000000..6b3540a --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/ComicWorkRepository.java @@ -0,0 +1,15 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface ComicWorkRepository extends JpaRepository { + + @Query("SELECT c from ComicWork c where c.comic = ?1 and c.artist = ?2 and c.workType = ?3") + ComicWork findbyComicAndArtistAndWorktype(Comic comic, Artist artist, Worktype worktype); + + @Query("select c from ComicWork c where c.comic = ?1") + List findByComic(Comic comic); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/Issue.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Issue.java new file mode 100644 index 0000000..5a36f7d --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Issue.java @@ -0,0 +1,42 @@ +package de.thpeetz.kontor.comics.data; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import io.micrometer.common.lang.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper = false) +@Entity +public class Issue extends AbstractEntity { + + @ManyToOne + @JoinColumn(name = "comic_id") + @NotNull + @JsonIgnoreProperties({ "issues" }) + private Comic comic; + + @ManyToOne + @JoinColumn(name = "volume_id") + @JsonIgnoreProperties({ "issues" }) + @Nullable + private Volume volume; + + @NotEmpty + private String issueNumber; + + private Boolean isRead; + + private Boolean inStock; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/IssueRepository.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/IssueRepository.java new file mode 100644 index 0000000..af10705 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/IssueRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface IssueRepository extends JpaRepository { + @Query("select i from Issue i " + + "where lower(i.issueNumber) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + List findByComic(Comic comic); + + Issue findByComicAndIssueNumber(Comic comic, String issueNumber); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/Publisher.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Publisher.java new file mode 100644 index 0000000..233d405 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Publisher.java @@ -0,0 +1,40 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.LinkedList; +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotEmpty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +public class Publisher extends AbstractEntity { + + @NotEmpty + @Column(unique = true) + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "publisher", cascade = CascadeType.ALL, orphanRemoval = true) + @Nullable + private List comics = new LinkedList<>(); + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("Publisher{"); + sb.append("name='").append(name).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/PublisherRepository.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/PublisherRepository.java new file mode 100644 index 0000000..5b5587d --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/PublisherRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface PublisherRepository extends JpaRepository { + @Query("select p from Publisher p " + + "where lower(p.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + Publisher findByName(String name); + + List findByNameIgnoreCase(String name); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/StoryArc.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/StoryArc.java new file mode 100644 index 0000000..34f2188 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/StoryArc.java @@ -0,0 +1,31 @@ +package de.thpeetz.kontor.comics.data; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper = false) +@Entity +public class StoryArc extends AbstractEntity { + + @NotEmpty + private String name; + + @ManyToOne + @JoinColumn(name = "comic_id") + @NotNull + @JsonIgnoreProperties({ "storyArcs" }) + private Comic comic; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/StoryArcRepository.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/StoryArcRepository.java new file mode 100644 index 0000000..a74619f --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/StoryArcRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface StoryArcRepository extends JpaRepository { + @Query("select s from StoryArc s " + + "where lower(s.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + StoryArc findByNameAndComic(String name, Comic comic); + + List findByComic(Comic comic); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/TradePaperback.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/TradePaperback.java new file mode 100644 index 0000000..07ee8d3 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/TradePaperback.java @@ -0,0 +1,32 @@ +package de.thpeetz.kontor.comics.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper = false) +@Entity +public class TradePaperback extends AbstractEntity { + + @NotEmpty + private String name; + + @ManyToOne + @JoinColumn(name = "comic_id") + @NotNull + private Comic comic; + + private Integer issueStart; + + private Integer issueEnd; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/TradePaperbackRepository.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/TradePaperbackRepository.java new file mode 100644 index 0000000..fba26ad --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/TradePaperbackRepository.java @@ -0,0 +1,20 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface TradePaperbackRepository extends JpaRepository { + @Query("select t from TradePaperback t " + + "where lower(t.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + List findByComic(Comic comic); + + List findByNameAndComic(String name, Comic comic); + + @Query("select t from TradePaperback t where t.name = ?1 and t.comic = ?2 and t.issueStart = ?3 and t.issueEnd = ?4") + TradePaperback findByFields(String name, Comic comic, int start, int end); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/Volume.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Volume.java new file mode 100644 index 0000000..c8e1eb0 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Volume.java @@ -0,0 +1,42 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.LinkedList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import io.micrometer.common.lang.Nullable; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper = false) +@Entity +public class Volume extends AbstractEntity { + + @NotEmpty + private String name; + + @ManyToOne + @JoinColumn(name = "comic_id") + @NotNull + @JsonIgnoreProperties({ "volumes" }) + private Comic comic; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "volume", cascade = CascadeType.REMOVE, orphanRemoval = true) + @Nullable + private List issues = new LinkedList<>(); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/VolumeRepository.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/VolumeRepository.java new file mode 100644 index 0000000..3b70670 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/VolumeRepository.java @@ -0,0 +1,13 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface VolumeRepository extends JpaRepository { + + List findByName(String name); + + List findByComic(Comic comic); + +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/Worktype.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Worktype.java new file mode 100644 index 0000000..9c83133 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Worktype.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.LinkedList; +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import io.micrometer.common.lang.Nullable; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotEmpty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Represents a work type in the application. + * This class extends the AbstractEntity class. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +public class Worktype extends AbstractEntity { + + @NotEmpty + @Column(unique = true) + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "workType", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + List comicWorks = new LinkedList<>(); + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("Worktype{"); + sb.append("name='").append(name).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/WorktypeRepository.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/WorktypeRepository.java new file mode 100644 index 0000000..2afebaf --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/WorktypeRepository.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface WorktypeRepository extends JpaRepository { + @Query("select w from Worktype w " + + "where lower(w.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + + Worktype findByName(String name); + + List findByNameIgnoreCase(String name); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/services/ComicService.java b/springboot/src/main/java/de/thpeetz/kontor/comics/services/ComicService.java new file mode 100644 index 0000000..ae4637f --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/services/ComicService.java @@ -0,0 +1,299 @@ +package de.thpeetz.kontor.comics.services; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.ArtistRepository; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.ComicRepository; +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.data.ComicWorkRepository; +import de.thpeetz.kontor.comics.data.Issue; +import de.thpeetz.kontor.comics.data.IssueRepository; +import de.thpeetz.kontor.comics.data.Publisher; +import de.thpeetz.kontor.comics.data.PublisherRepository; +import de.thpeetz.kontor.comics.data.StoryArc; +import de.thpeetz.kontor.comics.data.StoryArcRepository; +import de.thpeetz.kontor.comics.data.TradePaperback; +import de.thpeetz.kontor.comics.data.TradePaperbackRepository; +import de.thpeetz.kontor.comics.data.Volume; +import de.thpeetz.kontor.comics.data.VolumeRepository; +import de.thpeetz.kontor.comics.data.Worktype; +import de.thpeetz.kontor.comics.data.WorktypeRepository; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class ComicService { + + private final PublisherRepository publisherRepository; + private final ComicRepository comicRepository; + private final ArtistRepository artistRepository; + private final IssueRepository issueRepository; + private final StoryArcRepository storyArcRepository; + private final TradePaperbackRepository tradePaperbackRepository; + private final ComicWorkRepository comicWorkRepository; + private final VolumeRepository volumeRepository; + private final WorktypeRepository worktypeRepository; + + public ComicService(PublisherRepository publisherRepository, ComicRepository comicRepository, + ArtistRepository artistRepository, IssueRepository issueRepository, StoryArcRepository storyArcRepository, + TradePaperbackRepository tradePaperbackRepository, ComicWorkRepository comicWorkRepository, + VolumeRepository volumeRepository, WorktypeRepository worktypeRepository) { + + this.publisherRepository = publisherRepository; + this.comicRepository = comicRepository; + this.artistRepository = artistRepository; + this.issueRepository = issueRepository; + this.storyArcRepository = storyArcRepository; + this.tradePaperbackRepository = tradePaperbackRepository; + this.comicWorkRepository = comicWorkRepository; + this.volumeRepository = volumeRepository; + this.worktypeRepository = worktypeRepository; + } + + public List findAllPublishers(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return publisherRepository.findAll(); + } else { + return publisherRepository.search(stringFilter); + } + } + + public Publisher findPublisherByName(String publisherName) { + return publisherRepository.findByName(publisherName); + } + + public void deletePublisher(Publisher publisher) { + publisherRepository.delete(publisher); + } + + public void savePublisher(Publisher publisher) { + if (publisher == null) { + log.warn("Publisher is null. Are you sure you have connected your form to the application?"); + return; + } + publisherRepository.save(publisher); + } + + public List findAllComics(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return comicRepository.findAll(); + } else { + return comicRepository.search(stringFilter); + } + } + + public Comic findComicByTitle(String title) { + List comics = comicRepository.findByTitle(title); + if (comics.size() == 1) { + return comics.get(0); + } + return null; + } + + public void deleteComic(Comic comic) { + Publisher publisher = comic.getPublisher(); + publisher.getComics().remove(comic); + publisherRepository.save(publisher); + comicRepository.delete(comic); + } + + public void saveComic(Comic comic) { + if (comic == null) { + log.warn("Comic is null. Are you sure you have connected your form to the application?"); + return; + } + comicRepository.save(comic); + } + + public List findAllArtists(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return artistRepository.findAll(); + } else { + return artistRepository.search(stringFilter); + } + } + + public Artist findArtistByName(String artistName) { + if (artistName == null || artistName.isEmpty()) { + return null; + } else { + return artistRepository.findByName(artistName); + } + } + + public void deleteArtist(Artist artist) { + artistRepository.delete(artist); + } + + public void saveArtist(Artist artist) { + if (artist == null) { + log.warn("Artist is null. Are you sure you have connected your form to the application?"); + return; + } + artistRepository.save(artist); + } + + public List findAllIssues() { + return issueRepository.findAll(); + } + + public List findAllIssuesForComic(Comic comic) { + if (comic == null) { + return issueRepository.findAll(); + } else { + log.info("Find issues for Comic: {}", comic); + return issueRepository.findByComic(comic); + } + } + + public void saveIssue(Issue issue) { + if (issue == null) { + log.warn("Issue is null. Are you sure you have connected your form to the application?"); + return; + } + issueRepository.save(issue); + } + + public void deleteIssue(Issue issue) { + issueRepository.delete(issue); + } + + public List findAllStoryArcs() { + return storyArcRepository.findAll(); + } + + public List findAllStoryArcsForComic(Comic comic) { + if (comic == null) { + return storyArcRepository.findAll(); + } else { + log.info("Find Story Arc for Comic: {}", comic); + return storyArcRepository.findByComic(comic); + } + } + + public void saveStoryArc(StoryArc storyArc) { + storyArcRepository.save(storyArc); + } + + public void deleteStoryArc(StoryArc storyArc) { + Comic comic = storyArc.getComic(); + comic.getStoryArcs().remove(storyArc); + comicRepository.save(comic); + storyArcRepository.delete(storyArc); + } + + public List findAllTradePaperbacks(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return tradePaperbackRepository.findAll(); + } else { + return tradePaperbackRepository.search(stringFilter); + } + } + + public void deleteTradePaperBack(TradePaperback tradepaperback) { + tradePaperbackRepository.delete(tradepaperback); + } + + public void saveTradePaperBack(TradePaperback tradepaperback) { + if (tradepaperback == null) { + log.warn("TradePaperBack is null. Are you sure you have connected your form to the application?"); + return; + } + tradePaperbackRepository.save(tradepaperback); + } + + public List findAllComicWorks() { + return comicWorkRepository.findAll(); + } + + public void saveComicWork(ComicWork comicWork) { + if (comicWork == null) { + log.warn("ComicWork is null. Are you sure you have connected your form to the application?"); + return; + } + comicWorkRepository.save(comicWork); + } + + public void deleteComicWork(ComicWork comicWork) { + Comic comic = comicWork.getComic(); + comic.getComicWorks().remove(comicWork); + comicRepository.save(comic); + Artist artist = comicWork.getArtist(); + artist.getComicWorks().remove(comicWork); + artistRepository.save(artist); + Worktype worktype = comicWork.getWorkType(); + worktype.getComicWorks().remove(comicWork); + worktypeRepository.save(worktype); + comicWorkRepository.delete(comicWork); + } + + public List findAllVolumes() { + return volumeRepository.findAll(); + } + + public List findAllVolumesForComic(Comic comic) { + if (comic == null) { + return volumeRepository.findAll(); + } else { + log.info("Find Volume for Comic: {}", comic); + return volumeRepository.findByComic(comic); + } + } + + public void saveVolume(Volume volume) { + if (volume == null) { + log.warn("Volume is null. Are you sure you have connected your form to the application?"); + return; + } + volumeRepository.save(volume); + } + + public void deleteVolume(Volume volume) { + Comic comic = volume.getComic(); + comic.getVolumes().remove(volume); + comicRepository.save(comic); + volumeRepository.delete(volume); + } + + public List findAllWorktypes(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return worktypeRepository.findAll(); + } else { + return worktypeRepository.search(stringFilter); + } + } + + public Worktype findWorktypeByName(String worktypeName) { + if (worktypeName == null || worktypeName.isEmpty()) { + return null; + } else { + return worktypeRepository.findByName(worktypeName); + } + } + + public void saveWorktype(Worktype worktype) { + if (worktype == null) { + log.warn("Worktype is null. Are you sure you have connected your form to the application?"); + return; + } + worktypeRepository.save(worktype); + } + + public void deleteWorktype(Worktype worktype) { + List comicWorks = worktype.getComicWorks(); + if (comicWorks == null) { + log.warn("reference to ComicWork is null"); + return; + } + log.info("found {} references to ComicWork", comicWorks.size()); + comicWorks.forEach(comicWork -> { + comicWork.setWorkType(null); + }); + log.info("delete Worktype: {}", worktype); + worktypeRepository.delete(worktype); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistForm.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistForm.java new file mode 100644 index 0000000..43a93d4 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistForm.java @@ -0,0 +1,117 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.ComicWork; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ArtistForm extends FormLayout { + + TextField name = new TextField("Name"); + Grid comicWorks = new Grid<>(ComicWork.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Artist.class); + + public ArtistForm() { + addClassName("artist-form"); + binder.bindInstanceFields(this); + + comicWorks.setColumns("workType.name", "comic.title"); + comicWorks.getColumnByKey("workType.name").setHeader("Work type"); + comicWorks.getColumnByKey("comic.title").setHeader("Comic"); + comicWorks.getColumns().forEach(col -> col.setAutoWidth(true)); + add(name, comicWorks, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setArtist(Artist artist) { + binder.setBean(artist); + } + + public void setComicWorks(List works) { + log.info("Setting comic works: {}", works); + this.comicWorks.setItems(works); + } + + public abstract static class ArtistFormEvent extends ComponentEvent { + private Artist artist; + + protected ArtistFormEvent(ArtistForm source, Artist artist) { + super(source, false); + this.artist = artist; + } + + public Artist getArtist() { + return artist; + } + } + + public static class SaveEvent extends ArtistFormEvent { + SaveEvent(ArtistForm source, Artist artist) { + super(source, artist); + } + } + + public static class DeleteEvent extends ArtistFormEvent { + DeleteEvent(ArtistForm source, Artist artist) { + super(source, artist); + } + } + + public static class CloseEvent extends ArtistFormEvent { + CloseEvent(ArtistForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java new file mode 100644 index 0000000..f0d294d --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java @@ -0,0 +1,129 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = "artist", layout = MainLayout.class) +@PageTitle("Artist | Comics | Kontor") +public class ArtistView extends VerticalLayout { + + Grid grid = new Grid<>(Artist.class); + TextField filterText = new TextField(); + ArtistForm form; + ComicService service; + + public ArtistView(ComicService service) { + this.service = service; + addClassName("artist-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + public ArtistForm getForm() { + return form; + } + + private void configureGrid() { + grid.addClassName("artist-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editArtist(event.getValue())); + } + + private void configureForm() { + form = new ArtistForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveArtist); + form.addDeleteListener(this::deleteArtist); + form.addCloseListener(e -> closeEditor()); + } + + private void saveArtist(ArtistForm.SaveEvent event) { + service.saveArtist(event.getArtist()); + updateList(); + closeEditor(); + } + + private void deleteArtist(ArtistForm.DeleteEvent event) { + service.deleteArtist(event.getArtist()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addArtistButton = new Button("Add artist"); + addArtistButton.addClickListener(click -> addArtist()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addArtistButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editArtist(Artist artist) { + if (artist == null) { + closeEditor(); + } else { + form.setArtist(artist); + form.setComicWorks(artist.getComicWorks()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setArtist(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addArtist() { + grid.asSingleSelect().clear(); + editArtist(new Artist()); + } + + public void updateList() { + grid.setItems(service.findAllArtists(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicForm.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicForm.java new file mode 100644 index 0000000..55fc557 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicForm.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.data.Publisher; + +public class ComicForm extends FormLayout { + + private static final Logger log = LoggerFactory.getLogger(ComicForm.class); + + TextField title = new TextField("Title"); + ComboBox publisher = new ComboBox<>("Publisher"); + Checkbox currentOrder = new Checkbox("Current order"); + Checkbox completed = new Checkbox("Completed"); + Grid comicWorks = new Grid<>(ComicWork.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Comic.class); + + public ComicForm(List publishers) { + addClassName("comic-form"); + binder.bindInstanceFields(this); + + publisher.setItems(publishers); + publisher.setItemLabelGenerator(Publisher::getName); + // comicWorks.addClassName("comic-works-grid"); + // comicWorks.setSizeFull(); + comicWorks.setColumns("workType.name", "artist.name"); + comicWorks.getColumnByKey("workType.name").setHeader("Work type"); + comicWorks.getColumnByKey("artist.name").setHeader("Artist"); + comicWorks.getColumns().forEach(col -> col.setAutoWidth(true)); + add(title, publisher, currentOrder, completed, comicWorks, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setComic(Comic comic) { + binder.setBean(comic); + } + + public void setComicWorks(List works) { + log.info("Setting comic works: {}", works); + comicWorks.setItems(works); + } + + public abstract static class ComicFormEvent extends ComponentEvent { + private Comic comic; + + protected ComicFormEvent(ComicForm source, Comic comic) { + super(source, false); + this.comic = comic; + } + + public Comic getComic() { + return comic; + } + } + + public static class SaveEvent extends ComicFormEvent { + SaveEvent(ComicForm source, Comic comic) { + super(source, comic); + } + } + + public static class DeleteEvent extends ComicFormEvent { + DeleteEvent(ComicForm source, Comic comic) { + super(source, comic); + } + } + + public static class CloseEvent extends ComicFormEvent { + CloseEvent(ComicForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicLayout.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicLayout.java new file mode 100644 index 0000000..cffdbdb --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicLayout.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.comics.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.common.views.KontorLayoutUtil; +import de.thpeetz.kontor.security.SecurityService; +import lombok.extern.slf4j.Slf4j; + +/** + * Represents a custom layout for the comic view in the application. + * This layout extends the AppLayout class. + */ +@Slf4j +public class ComicLayout extends AppLayout { + + private final AdminService adminService; + + private final SecurityService securityService; + + public ComicLayout(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + + KontorLayoutUtil layout = new KontorLayoutUtil(this, adminService, securityService); + layout.setSecondaryNavigation(getSecondaryNavigation()); + layout.createHeader(ComicConstants.COMICS); + } + + private HorizontalLayout getSecondaryNavigation() { + HorizontalLayout navigation = new HorizontalLayout(); + navigation.addClassNames(LumoUtility.JustifyContent.CENTER, LumoUtility.Gap.SMALL, LumoUtility.Height.MEDIUM); + navigation.add(ComicConstants.getComicLink(), ComicConstants.getPublisherLink(), ComicConstants.getIssueLink(), + ComicConstants.getTradePaperbackLink(), ComicConstants.getStoryArcLink(), + ComicConstants.getVolumeLink(), ComicConstants.getArtistLink(), ComicConstants.getComicWorkLink(), + ComicConstants.getWorktypeLink()); + return navigation; + } +} + diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicView.java new file mode 100644 index 0000000..ec09544 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicView.java @@ -0,0 +1,139 @@ +package de.thpeetz.kontor.comics.views; + +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; + +@Slf4j +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.COMICS_ROUTE, layout = MainLayout.class) +@PageTitle("Comic | Comics | Kontor") +public class ComicView extends VerticalLayout { + + Grid grid = new Grid<>(Comic.class); + TextField filterText = new TextField(); + ComicForm form; + ComicService service; + + public ComicView(ComicService service) { + this.service = service; + addClassName("comic-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("comic-grid"); + grid.setSizeFull(); + grid.setColumns("title", "publisher.name", "currentOrder", "completed"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editComic(event.getValue())); + } + + public ComicForm getForm() { + return form; + } + + private void configureForm() { + form = new ComicForm(service.findAllPublishers(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveComic); + form.addDeleteListener(this::deleteComic); + form.addCloseListener(e -> closeEditor()); + } + + private void saveComic(ComicForm.SaveEvent event) { + service.saveComic(event.getComic()); + updateList(); + closeEditor(); + } + + private void deleteComic(ComicForm.DeleteEvent event) { + service.deleteComic(event.getComic()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addComicButton = new Button("Add comic"); + addComicButton.addClickListener(click -> addComic()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addComicButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editComic(Comic comic) { + if (comic == null) { + closeEditor(); + } else { + form.setComic(comic); + if (comic.getComicWorks() == null) { + log.info("No comic works"); + } else { + log.info("Comic works sze: {}", comic.getComicWorks().size()); + } + form.setComicWorks(comic.getComicWorks()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setComic(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addComic() { + grid.asSingleSelect().clear(); + editComic(new Comic()); + } + + public void updateList() { + grid.setItems(service.findAllComics(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkForm.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkForm.java new file mode 100644 index 0000000..698a37e --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkForm.java @@ -0,0 +1,113 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.data.Worktype; + +public class ComicWorkForm extends FormLayout { + ComboBox comic = new ComboBox<>("Comic"); + ComboBox artist = new ComboBox<>("Artist"); + ComboBox workType = new ComboBox<>("Worktype"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(ComicWork.class); + + public ComicWorkForm(List comics, List artists, List workTypes) { + addClassName("comicwork-form"); + binder.bindInstanceFields(this); + + comic.setItems(comics); + comic.setItemLabelGenerator(Comic::getTitle); + artist.setItems(artists); + artist.setItemLabelGenerator(Artist::getName); + workType.setItems(workTypes); + workType.setItemLabelGenerator(Worktype::getName); + add(comic, artist, workType, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setComicWork(ComicWork comicWork) { + binder.setBean(comicWork); + } + + public abstract static class ComicWorkFormEvent extends ComponentEvent { + private ComicWork comicWork; + + protected ComicWorkFormEvent(ComicWorkForm source, ComicWork comicWork) { + super(source, false); + this.comicWork = comicWork; + } + + public ComicWork getComicWork() { + return comicWork; + } + } + + public static class SaveEvent extends ComicWorkFormEvent { + SaveEvent(ComicWorkForm source, ComicWork comicWork) { + super(source, comicWork); + } + } + + public static class DeleteEvent extends ComicWorkFormEvent { + DeleteEvent(ComicWorkForm source, ComicWork comicWork) { + super(source, comicWork); + } + } + + public static class CloseEvent extends ComicWorkFormEvent { + CloseEvent(ComicWorkForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkView.java new file mode 100644 index 0000000..449a76c --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkView.java @@ -0,0 +1,122 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.COMICWORK_ROUTE, layout = MainLayout.class) +@PageTitle("ComicWork | Comics | Kontor") +public class ComicWorkView extends VerticalLayout { + + Grid grid = new Grid<>(ComicWork.class); + ComicWorkForm form; + ComicService service; + + public ComicWorkView(ComicService service) { + this.service = service; + addClassName("comicWork-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("comic-grid"); + grid.setSizeFull(); + grid.setColumns("comic.title", "artist.name", "workType.name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editComicWork(event.getValue())); + } + + public ComicWorkForm getForm() { + return form; + } + + private void configureForm() { + form = new ComicWorkForm(service.findAllComics(null), service.findAllArtists(null), + service.findAllWorktypes(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveComicWork); + form.addDeleteListener(this::deleteComicWork); + form.addCloseListener(e -> closeEditor()); + } + + private void saveComicWork(ComicWorkForm.SaveEvent event) { + service.saveComicWork(event.getComicWork()); + updateList(); + closeEditor(); + } + + private void deleteComicWork(ComicWorkForm.DeleteEvent event) { + service.deleteComicWork(event.getComicWork()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + Button addComicButton = new Button("Add ComicWork"); + addComicButton.addClickListener(click -> addComicWork()); + + HorizontalLayout toolbar = new HorizontalLayout(addComicButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editComicWork(ComicWork comicWork) { + if (comicWork == null) { + closeEditor(); + } else { + form.setComicWork(comicWork); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setComicWork(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addComicWork() { + grid.asSingleSelect().clear(); + editComicWork(new ComicWork()); + } + + public void updateList() { + grid.setItems(service.findAllComicWorks()); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/IssueForm.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/IssueForm.java new file mode 100644 index 0000000..d537210 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/IssueForm.java @@ -0,0 +1,113 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.Issue; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class IssueForm extends FormLayout { + + ComboBox comic = new ComboBox<>("Comic"); + TextField issueNumber = new TextField("Issue number"); + Checkbox isRead = new Checkbox("Read"); + Checkbox inStock = new Checkbox("In stock"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Issue.class); + + public IssueForm(List comics) { + addClassName("issue-form"); + binder.bindInstanceFields(this); + + comic.setItems(comics); + comic.setItemLabelGenerator(Comic::getTitle); + add(comic, issueNumber, isRead, inStock, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setIssue(Issue issue) { + binder.setBean(issue); + } + + public abstract static class IssueFormEvent extends ComponentEvent { + private Issue issue; + + protected IssueFormEvent(IssueForm source, Issue issue) { + super(source, false); + this.issue = issue; + } + + public Issue getIssue() { + return issue; + } + } + + public static class SaveEvent extends IssueFormEvent { + SaveEvent(IssueForm source, Issue issue) { + super(source, issue); + } + } + + public static class DeleteEvent extends IssueFormEvent { + DeleteEvent(IssueForm source, Issue issue) { + super(source, issue); + } + } + + public static class CloseEvent extends IssueFormEvent { + CloseEvent(IssueForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/IssueView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/IssueView.java new file mode 100644 index 0000000..4b9d58c --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/IssueView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.Issue; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.ISSUE_ROUTE, layout = MainLayout.class) +@PageTitle("Issue | Comics | Kontor") +public class IssueView extends VerticalLayout { + + Grid grid = new Grid<>(Issue.class); + ComboBox comicFilter = new ComboBox<>("Comic"); + IssueForm form; + ComicService service; + + public IssueView(ComicService service) { + this.service = service; + addClassName("issue-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("issue-grid"); + grid.setSizeFull(); + grid.setColumns("comic.title", "issueNumber", "isRead", "inStock"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editIssue(event.getValue())); + } + + public IssueForm getForm() { + return form; + } + + private void configureForm() { + form = new IssueForm(service.findAllComics(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveIssue); + form.addDeleteListener(this::deleteIssue); + form.addCloseListener(e -> closeEditor()); + } + + private void saveIssue(IssueForm.SaveEvent event) { + service.saveIssue(event.getIssue()); + updateList(); + closeEditor(); + } + + private void deleteIssue(IssueForm.DeleteEvent event) { + service.deleteIssue(event.getIssue()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + comicFilter.setItems(service.findAllComics(null)); + comicFilter.setItemLabelGenerator(Comic::getTitle); + comicFilter.addValueChangeListener(e -> updateList()); + comicFilter.setClearButtonVisible(true); + + Button addIssueButton = new Button("Add issue", click -> addIssue()); + + HorizontalLayout toolbar = new HorizontalLayout(comicFilter, addIssueButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editIssue(Issue issue) { + if (issue == null) { + closeEditor(); + } else { + form.setIssue(issue); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setIssue(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addIssue() { + grid.asSingleSelect().clear(); + editIssue(new Issue()); + } + + private void updateList() { + grid.setItems(service.findAllIssuesForComic(comicFilter.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/PublisherForm.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/PublisherForm.java new file mode 100644 index 0000000..dbd2f62 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/PublisherForm.java @@ -0,0 +1,100 @@ +package de.thpeetz.kontor.comics.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Publisher; + +public class PublisherForm extends FormLayout { + public TextField name = new TextField("Name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Publisher.class); + + public PublisherForm() { + addClassName("publisher-form"); + binder.bindInstanceFields(this); + + add(name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setPublisher(Publisher publisher) { + binder.setBean(publisher); + } + + public abstract static class PublisherFormEvent extends ComponentEvent { + private Publisher publisher; + + protected PublisherFormEvent(PublisherForm source, Publisher publisher) { + super(source, false); + this.publisher = publisher; + } + + public Publisher getPublisher() { + return publisher; + } + } + + public static class SaveEvent extends PublisherFormEvent { + SaveEvent(PublisherForm source, Publisher publisher) { + super(source, publisher); + } + } + + public static class DeleteEvent extends PublisherFormEvent { + DeleteEvent(PublisherForm source, Publisher publisher) { + super(source, publisher); + } + } + + public static class CloseEvent extends PublisherFormEvent { + CloseEvent(PublisherForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/PublisherView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/PublisherView.java new file mode 100644 index 0000000..b716889 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/PublisherView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.Publisher; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.PUBLISHER_ROUTE, layout = MainLayout.class) +@PageTitle("Publisher | Comics | Kontor") +public class PublisherView extends VerticalLayout { + + Grid grid = new Grid<>(Publisher.class); + TextField filterText = new TextField(); + PublisherForm form; + + ComicService service; + + public PublisherView(ComicService service) { + this.service = service; + addClassName("publisher-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("publisher-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editPublisher(event.getValue())); + } + + private void configureForm() { + form = new PublisherForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::savePublisher); + form.addDeleteListener(this::deletePublisher); + form.addCloseListener(e -> closeEditor()); + } + + private void savePublisher(PublisherForm.SaveEvent event) { + service.savePublisher(event.getPublisher()); + updateList(); + closeEditor(); + } + + private void deletePublisher(PublisherForm.DeleteEvent event) { + service.deletePublisher(event.getPublisher()); + updateList(); + closeEditor(); + } + + public Grid getGrid() { + return grid; + } + + public PublisherForm getForm() { + return form; + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addPublisherButton = new Button("Add publisher"); + addPublisherButton.addClickListener(click -> addPublisher()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addPublisherButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editPublisher(Publisher publisher) { + if (publisher == null) { + closeEditor(); + } else { + form.setPublisher(publisher); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setPublisher(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addPublisher() { + grid.asSingleSelect().clear(); + editPublisher(new Publisher()); + } + + public void updateList() { + grid.setItems(service.findAllPublishers(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/StoryArcForm.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/StoryArcForm.java new file mode 100644 index 0000000..786a792 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/StoryArcForm.java @@ -0,0 +1,108 @@ +package de.thpeetz.kontor.comics.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.StoryArc; + +import java.util.List; + +public class StoryArcForm extends FormLayout { + + ComboBox comic = new ComboBox<>("Comic"); + TextField name = new TextField("Story Arc Name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(StoryArc.class); + + public StoryArcForm(List comics) { + addClassName("storyarc-form"); + binder.bindInstanceFields(this); + + comic.setItems(comics); + comic.setItemLabelGenerator(Comic::getTitle); + add(comic, name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setStoryArc(StoryArc storyArc) { + binder.setBean(storyArc); + } + + public abstract static class StoryArcFormEvent extends ComponentEvent { + private StoryArc storyArc; + + protected StoryArcFormEvent(StoryArcForm source, StoryArc storyArc) { + super(source, false); + this.storyArc = storyArc; + } + + public StoryArc getStoryArc() { + return storyArc; + } + } + + public static class SaveEvent extends StoryArcFormEvent { + SaveEvent(StoryArcForm source, StoryArc storyArc) { + super(source, storyArc); + } + } + + public static class DeleteEvent extends StoryArcFormEvent { + DeleteEvent(StoryArcForm source, StoryArc storyArc) { + super(source, storyArc); + } + } + + public static class CloseEvent extends StoryArcFormEvent { + CloseEvent(StoryArcForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/StoryArcView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/StoryArcView.java new file mode 100644 index 0000000..6044383 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/StoryArcView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.StoryArc; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.STORYARC_ROTE, layout = MainLayout.class) +@PageTitle("StoryArc | Comics | Kontor") +public class StoryArcView extends VerticalLayout { + + Grid grid = new Grid<>(StoryArc.class); + ComboBox comicFilter = new ComboBox<>("Comic"); + StoryArcForm form; + ComicService service; + + public StoryArcView(ComicService service) { + this.service = service; + addClassName("storyarc-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + StoryArcForm getForm() { + return form; + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("storyarc-grid"); + grid.setSizeFull(); + grid.setColumns("comic.title", "name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editStoryArc(event.getValue())); + } + + private void configureForm() { + form = new StoryArcForm(service.findAllComics(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveStoryArc); + form.addDeleteListener(this::deleteStoryArc); + form.addCloseListener(e -> closeEditor()); + } + + private void saveStoryArc(StoryArcForm.SaveEvent event) { + service.saveStoryArc(event.getStoryArc()); + updateList(); + closeEditor(); + } + + private void deleteStoryArc(StoryArcForm.DeleteEvent event) { + service.deleteStoryArc(event.getStoryArc()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + comicFilter.setItems(service.findAllComics(null)); + comicFilter.setItemLabelGenerator(Comic::getTitle); + comicFilter.addValueChangeListener(e -> updateList()); + comicFilter.setClearButtonVisible(true); + + Button addStoryArcButton = new Button("Add StoryArc", click -> addStoryArc()); + + HorizontalLayout toolbar = new HorizontalLayout(comicFilter, addStoryArcButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editStoryArc(StoryArc storyArc) { + if (storyArc == null) { + closeEditor(); + } else { + form.setStoryArc(storyArc); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setStoryArc(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addStoryArc() { + grid.asSingleSelect().clear(); + editStoryArc(new StoryArc()); + } + + private void updateList() { + grid.setItems(service.findAllStoryArcsForComic(comicFilter.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/TradePaperBackForm.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/TradePaperBackForm.java new file mode 100644 index 0000000..8a1b477 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/TradePaperBackForm.java @@ -0,0 +1,109 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.TradePaperback; + +public class TradePaperBackForm extends FormLayout { + TextField name = new TextField("Name"); + ComboBox comic = new ComboBox<>("Comic"); + TextField issueStart = new TextField("Issue Start"); + TextField issueEnd = new TextField("Issue End"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(TradePaperback.class); + + public TradePaperBackForm(List comics) { + addClassName("tradepaperback-form"); + binder.bindInstanceFields(this); + + comic.setItems(comics); + comic.setItemLabelGenerator(Comic::getTitle); + add(name, comic, issueStart, issueEnd, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setTradePaperBack(TradePaperback tradepaperback) { + binder.setBean(tradepaperback); + } + + public abstract static class TradePaperBackFormEvent extends ComponentEvent { + private TradePaperback tradepaperback; + + protected TradePaperBackFormEvent(TradePaperBackForm source, TradePaperback tradepaperback) { + super(source, false); + this.tradepaperback = tradepaperback; + } + + public TradePaperback getTradePaperBack() { + return tradepaperback; + } + } + + public static class SaveEvent extends TradePaperBackFormEvent { + SaveEvent(TradePaperBackForm source, TradePaperback tradepaperback) { + super(source, tradepaperback); + } + } + + public static class DeleteEvent extends TradePaperBackFormEvent { + DeleteEvent(TradePaperBackForm source, TradePaperback tradepaperback) { + super(source, tradepaperback); + } + } + + public static class CloseEvent extends TradePaperBackFormEvent { + CloseEvent(TradePaperBackForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/TradePaperbackView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/TradePaperbackView.java new file mode 100644 index 0000000..0215234 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/TradePaperbackView.java @@ -0,0 +1,129 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.TradePaperback; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.TPB_ROUTE, layout = MainLayout.class) +@PageTitle("TradePaperBack | Comics | Kontor") +public class TradePaperbackView extends VerticalLayout { + + Grid grid = new Grid<>(TradePaperback.class); + TextField filterText = new TextField(); + TradePaperBackForm form; + ComicService service; + + public TradePaperbackView(ComicService service) { + this.service = service; + addClassName("tradepaperback-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("tradepaperback-grid"); + grid.setSizeFull(); + grid.setColumns("name", "comic.title", "issueStart", "issueEnd"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editTradePaperBack(event.getValue())); + } + + public TradePaperBackForm getForm() { + return form; + } + + private void configureForm() { + form = new TradePaperBackForm(service.findAllComics(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveTradePaperBack); + form.addDeleteListener(this::deleteTradePaperBack); + form.addCloseListener(e -> closeEditor()); + } + + private void saveTradePaperBack(TradePaperBackForm.SaveEvent event) { + service.saveTradePaperBack(event.getTradePaperBack()); + updateList(); + closeEditor(); + } + + private void deleteTradePaperBack(TradePaperBackForm.DeleteEvent event) { + service.deleteTradePaperBack(event.getTradePaperBack()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addTradePaperBackButton = new Button("Add TradePaperBack"); + addTradePaperBackButton.addClickListener(click -> addTradePaperBack()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addTradePaperBackButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editTradePaperBack(TradePaperback tradepaperback) { + if (tradepaperback == null) { + closeEditor(); + } else { + form.setTradePaperBack(tradepaperback); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setTradePaperBack(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addTradePaperBack() { + grid.asSingleSelect().clear(); + editTradePaperBack(new TradePaperback()); + } + + public void updateList() { + grid.setItems(service.findAllTradePaperbacks(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/VolumeForm.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/VolumeForm.java new file mode 100644 index 0000000..9b333a9 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/VolumeForm.java @@ -0,0 +1,109 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.StoryArc; +import de.thpeetz.kontor.comics.data.Volume; + +public class VolumeForm extends FormLayout { + + ComboBox comic = new ComboBox<>("Comic"); + public TextField name = new TextField("Name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Volume.class); + + public VolumeForm(List comics) { + addClassName("volume-form"); + binder.bindInstanceFields(this); + + comic.setItems(comics); + comic.setItemLabelGenerator(Comic::getTitle); + add(comic, name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setVolume(Volume volume) { + binder.setBean(volume); + } + + public abstract static class VolumeFormEvent extends ComponentEvent { + private Volume volume; + + protected VolumeFormEvent(VolumeForm source, Volume volume) { + super(source, false); + this.volume = volume; + } + + public Volume getVolume() { + return volume; + } + } + + public static class SaveEvent extends VolumeFormEvent { + SaveEvent(VolumeForm source, Volume volume) { + super(source, volume); + } + } + + public static class DeleteEvent extends VolumeFormEvent { + DeleteEvent(VolumeForm source, Volume volume) { + super(source, volume); + } + } + + public static class CloseEvent extends VolumeFormEvent { + CloseEvent(VolumeForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/VolumeView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/VolumeView.java new file mode 100644 index 0000000..47a2201 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/VolumeView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.Volume; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.VOLUME_ROUTE, layout = MainLayout.class) +@PageTitle("Volume | Comics | Kontor") +public class VolumeView extends VerticalLayout { + + Grid grid = new Grid<>(Volume.class); + ComboBox comicFilter = new ComboBox<>("Comic"); + VolumeForm form; + ComicService service; + + public VolumeView(ComicService service) { + this.service = service; + addClassName("volume-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + VolumeForm getForm() { + return form; + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("volume-grid"); + grid.setSizeFull(); + grid.setColumns("comic.title", "name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editVolume(event.getValue())); + } + + private void configureForm() { + form = new VolumeForm(service.findAllComics(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveVolume); + form.addDeleteListener(this::deleteVolume); + form.addCloseListener(e -> closeEditor()); + } + + private void saveVolume(VolumeForm.SaveEvent event) { + service.saveVolume(event.getVolume()); + updateList(); + closeEditor(); + } + + private void deleteVolume(VolumeForm.DeleteEvent event) { + service.deleteVolume(event.getVolume()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + comicFilter.setItems(service.findAllComics(null)); + comicFilter.setItemLabelGenerator(Comic::getTitle); + comicFilter.addValueChangeListener(e -> updateList()); + comicFilter.setClearButtonVisible(true); + + Button addVolumeButton = new Button("Add Volume", click -> addVolume()); + + HorizontalLayout toolbar = new HorizontalLayout(comicFilter, addVolumeButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editVolume(Volume volume) { + if (volume == null) { + closeEditor(); + } else { + form.setVolume(volume); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setVolume(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addVolume() { + grid.asSingleSelect().clear(); + editVolume(new Volume()); + } + + private void updateList() { + grid.setItems(service.findAllVolumesForComic(comicFilter.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/WorktypeForm.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/WorktypeForm.java new file mode 100644 index 0000000..0965229 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/WorktypeForm.java @@ -0,0 +1,121 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.data.Worktype; + +public class WorktypeForm extends FormLayout { + + private static final Logger log = LoggerFactory.getLogger(WorktypeForm.class); + + TextField name = new TextField("Name"); + Grid comicWorks = new Grid<>(ComicWork.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Worktype.class); + + public WorktypeForm() { + addClassName("worktype-form"); + binder.bindInstanceFields(this); + + comicWorks.setColumns("comic.title", "artist.name"); + comicWorks.getColumnByKey("comic.title").setHeader("Comic"); + comicWorks.getColumnByKey("artist.name").setHeader("Artist"); + comicWorks.getColumns().forEach(col -> col.setAutoWidth(true)); + + add(name, comicWorks, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setWorktype(Worktype worktype) { + binder.setBean(worktype); + } + + public void setComicWorks(List works) { + log.info("Setting comic works: {}", works); + this.comicWorks.setItems(works); + } + + public abstract static class WorktypeFormEvent extends ComponentEvent { + private Worktype worktype; + + protected WorktypeFormEvent(WorktypeForm source, Worktype worktype) { + super(source, false); + this.worktype = worktype; + } + + public Worktype getWorktype() { + return worktype; + } + } + + public static class SaveEvent extends WorktypeFormEvent { + SaveEvent(WorktypeForm source, Worktype worktype) { + super(source, worktype); + } + } + + public static class DeleteEvent extends WorktypeFormEvent { + DeleteEvent(WorktypeForm source, Worktype worktype) { + super(source, worktype); + } + } + + public static class CloseEvent extends WorktypeFormEvent { + CloseEvent(WorktypeForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/WorktypeView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/WorktypeView.java new file mode 100644 index 0000000..98f3adb --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/WorktypeView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.Worktype; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.WORKTYPE_ROUTE, layout = MainLayout.class) +@PageTitle("Worktype | Comics | Kontor") +public class WorktypeView extends VerticalLayout { + + Grid grid = new Grid<>(Worktype.class); + TextField filterText = new TextField(); + WorktypeForm form; + ComicService service; + + public WorktypeView(ComicService service) { + this.service = service; + addClassName("worktype-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("worktype-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editWorktype(event.getValue())); + } + + public WorktypeForm getForm() { + return form; + } + + private void configureForm() { + form = new WorktypeForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveWorktype); + form.addDeleteListener(this::deleteWorktype); + form.addCloseListener(e -> closeEditor()); + } + + private void saveWorktype(WorktypeForm.SaveEvent event) { + service.saveWorktype(event.getWorktype()); + updateList(); + closeEditor(); + } + + private void deleteWorktype(WorktypeForm.DeleteEvent event) { + service.deleteWorktype(event.getWorktype()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addWorktypeButton = new Button("Add worktype"); + addWorktypeButton.addClickListener(click -> addWorktype()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addWorktypeButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editWorktype(Worktype worktype) { + if (worktype == null) { + closeEditor(); + } else { + form.setWorktype(worktype); + form.setComicWorks(worktype.getComicWorks()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setWorktype(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addWorktype() { + grid.asSingleSelect().clear(); + editWorktype(new Worktype()); + } + + public void updateList() { + grid.setItems(service.findAllWorktypes(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/common/data/AbstractEntity.java b/springboot/src/main/java/de/thpeetz/kontor/common/data/AbstractEntity.java new file mode 100644 index 0000000..f9cfdaf --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/common/data/AbstractEntity.java @@ -0,0 +1,34 @@ +package de.thpeetz.kontor.common.data; + +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.util.Date; + +@Slf4j +@Getter +@Setter +@EqualsAndHashCode +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class AbstractEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String id; + + @Version + private int version; + + @CreatedDate + private Date createdDate; + + @LastModifiedDate + private Date lastModifiedDate; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/common/data/AbstractLinkEntity.java b/springboot/src/main/java/de/thpeetz/kontor/common/data/AbstractLinkEntity.java new file mode 100644 index 0000000..7dfdf50 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/common/data/AbstractLinkEntity.java @@ -0,0 +1,55 @@ +package de.thpeetz.kontor.common.data; + +import jakarta.annotation.Nullable; +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.util.Date; + +@Slf4j +@Getter +@Setter +@EqualsAndHashCode +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class AbstractLinkEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String id; + + @Version + private int version; + + @CreatedDate + private Date createdDate; + + @LastModifiedDate + private Date lastModifiedDate; + + @Nullable + private String url; + + private boolean review; + + private boolean shouldDownload; + + @Nullable + private String title; + + @Nullable + private String cloudLink; + + @Nullable + private String fileName; + + @Nullable + private String path; + +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/common/views/AvatarMenuBar.java b/springboot/src/main/java/de/thpeetz/kontor/common/views/AvatarMenuBar.java new file mode 100644 index 0000000..1678e58 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/common/views/AvatarMenuBar.java @@ -0,0 +1,52 @@ +package de.thpeetz.kontor.common.views; + +import com.vaadin.flow.component.avatar.Avatar; +import com.vaadin.flow.component.contextmenu.MenuItem; +import com.vaadin.flow.component.contextmenu.SubMenu; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.menubar.MenuBar; +import com.vaadin.flow.component.menubar.MenuBarVariant; +import com.vaadin.flow.router.Route; + +import com.vaadin.flow.router.Router; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.admin.views.UserProfileView; +import de.thpeetz.kontor.security.SecurityService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; + +@Slf4j +@Route("avatar-menu-bar") +public class AvatarMenuBar extends Div { + + private final SecurityService securityService; + + final AdminService adminService; + + public AvatarMenuBar(SecurityService securityService, AdminService adminService) { + this.adminService = adminService; + this.securityService = securityService; + + Avatar avatar = new Avatar(); + securityService.getAuthenticatedUser().ifPresent(user -> { + log.info("AdminService: {}", adminService); + if (user != null) { + String userName = user.getUsername(); + String fullName = adminService.getUserFullName(userName); + avatar.setName(fullName); + } + }); + // avatar.setImage(pictureUrl); + MenuBar menuBar = new MenuBar(); + menuBar.addThemeVariants(MenuBarVariant.LUMO_TERTIARY_INLINE); + MenuItem menuItem = menuBar.addItem(avatar); + SubMenu subMenu = menuItem.getSubMenu(); + MenuItem logoutMenuItem = subMenu.addItem("Log out"); + logoutMenuItem.addClickListener(e -> securityService.logout()); + MenuItem profileItem = subMenu.addItem("Profile"); + profileItem.addClickListener(e -> profileItem.getUI().ifPresent(ui -> ui.navigate(UserProfileView.class))); + subMenu.addItem("Settings"); + subMenu.addItem("Help"); + add(menuBar); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/common/views/KontorLayoutUtil.java b/springboot/src/main/java/de/thpeetz/kontor/common/views/KontorLayoutUtil.java new file mode 100644 index 0000000..4c2112b --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/common/views/KontorLayoutUtil.java @@ -0,0 +1,100 @@ +package de.thpeetz.kontor.common.views; + +import com.vaadin.flow.component.accordion.Accordion; +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.applayout.AppLayout.Section; +import com.vaadin.flow.component.applayout.DrawerToggle; +import com.vaadin.flow.component.html.H1; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.Scroller; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.sidenav.SideNav; +import com.vaadin.flow.router.RouterLink; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.AdminConstants; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.security.SecurityService; +import de.thpeetz.kontor.tysc.TyscConstants; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class KontorLayoutUtil { + + private final AppLayout appLayout; + private HorizontalLayout secondaryNavigation; + + private AdminService adminService; + + private SecurityService securityService; + + public KontorLayoutUtil(AppLayout layout, AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + this.appLayout = layout; + } + + public void setSecondaryNavigation(HorizontalLayout secondaryNavigation) { + this.secondaryNavigation = secondaryNavigation; + } + + public void createHeader(String titleName) { + appLayout.addToDrawer(createTitle(), getScroller()); + appLayout.addToNavbar(getHeader(titleName)); + appLayout.setPrimarySection(Section.DRAWER); + } + + private H1 createTitle() { + H1 appTitle = new H1(new RouterLink("Kontor", MainView.class)); + appTitle.getStyle().set("font-size", "var(--lumo-font-size-l)") + .set("line-height", "var(--lumo-size-l)") + .set("margin", "0 var(--lumo-space-m)"); + return appTitle; + } + + private Scroller getScroller() { + Scroller scroller = new Scroller(getPrimaryNavigation()); + scroller.setClassName(LumoUtility.Padding.SMALL); + return scroller; + } + + private VerticalLayout getHeader(String header) { + DrawerToggle toggle = new DrawerToggle(); + H2 viewTitle = new H2(header); + viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE); + HorizontalLayout subViews = this.secondaryNavigation; + + AvatarMenuBar avatar = new AvatarMenuBar(securityService, adminService); + HorizontalLayout wrapper = new HorizontalLayout(toggle, viewTitle, avatar); + wrapper.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); + wrapper.expand(viewTitle); + wrapper.setWidthFull(); + wrapper.setSpacing(false); + + VerticalLayout viewHeader = new VerticalLayout(wrapper, subViews); + viewHeader.setPadding(false); + viewHeader.setSpacing(false); + return viewHeader; + } + + private SideNav getPrimaryNavigation() { + SideNav sideNav = new SideNav(); + sideNav.addItem(ComicConstants.getComicsNavigation()); + sideNav.addItem(TyscConstants.getTyscNavigation()); + sideNav.addItem(BookshelfConstants.getBookshelfNavigation()); + securityService.getAuthenticatedUser().ifPresent(user -> { + log.info("User {} found", user.getUsername()); + boolean isAdmin = user.getAuthorities().stream() + .anyMatch(grantedAuthority -> "ROLE_ADMIN".equals(grantedAuthority.getAuthority())); + log.info("User {} hat Admin-Rechte: {}", user, isAdmin); + if (isAdmin) { + sideNav.addItem(AdminConstants.getAdminNavigation()); + } + }); + return sideNav; + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/common/views/MainLayout.java b/springboot/src/main/java/de/thpeetz/kontor/common/views/MainLayout.java new file mode 100644 index 0000000..96f720f --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/common/views/MainLayout.java @@ -0,0 +1,101 @@ +package de.thpeetz.kontor.common.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.applayout.DrawerToggle; +import com.vaadin.flow.component.html.H1; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.Scroller; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.sidenav.SideNav; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.RouterLink; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.AdminConstants; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.mailclient.views.EmailView; +import de.thpeetz.kontor.media.MediaConstants; +import de.thpeetz.kontor.security.SecurityService; +import de.thpeetz.kontor.tysc.TyscConstants; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.GrantedAuthority; + +import java.util.ArrayList; +import java.util.Collection; + +@Slf4j +public class MainLayout extends AppLayout { + + private final AdminService adminService; + + private final SecurityService securityService; + + public MainLayout(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + + createHeader("Kontor"); + createDrawer(); + } + + public void createDrawer() { + H1 appTitle = new H1(new RouterLink("Kontor", MainView.class)); + appTitle.getStyle().set("font-size", "var(--lumo-font-size-l)") + .set("line-height", "var(--lumo-size-l)") + .set("margin", "0 var(--lumo-space-m)"); + Scroller scroller = new Scroller(getPrimaryNavigation()); + scroller.setClassName(LumoUtility.Padding.SMALL); + addToDrawer(appTitle, scroller); + } + + public void createHeader(String titleName) { + DrawerToggle toggle = new DrawerToggle(); + H2 viewTitle = new H2(titleName); + viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE); + + AvatarMenuBar avatar = new AvatarMenuBar(securityService, adminService); + HorizontalLayout wrapper = new HorizontalLayout(toggle, viewTitle, avatar); + wrapper.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); + wrapper.expand(viewTitle); + wrapper.setWidthFull(); + wrapper.setSpacing(false); + + VerticalLayout viewHeader = new VerticalLayout(wrapper); + viewHeader.setPadding(false); + viewHeader.setSpacing(false); + + addToNavbar(viewHeader); + setPrimarySection(Section.DRAWER); + } + + private SideNav getPrimaryNavigation() { + SideNav sideNav = new SideNav(); + ArrayList roles = new ArrayList<>(); + securityService.getAuthenticatedUser().ifPresent(user -> { + log.info("Get roles for user {}", user.getUsername()); + user.getAuthorities().forEach(grantedAuthority -> { + roles.add(grantedAuthority.getAuthority()); + }); + log.info("user {} has roles: {}", user, roles); + }); + sideNav.addItem(ComicConstants.getComicsNavigation()); + sideNav.addItem(TyscConstants.getTyscNavigation()); + sideNav.addItem(BookshelfConstants.getBookshelfNavigation()); + sideNav.addItem(MediaConstants.getMediaNavigation(roles)); + sideNav.addItem(new SideNavItem("Emails", EmailView.class)); + securityService.getAuthenticatedUser().ifPresent(user -> { + log.info("User {} found", user.getUsername()); + boolean isAdmin = user.getAuthorities().stream() + .anyMatch(grantedAuthority -> "ROLE_ADMIN".equals(grantedAuthority.getAuthority())); + log.info("User {} hat Admin-Rechte: {}", user, isAdmin); + if (isAdmin) { + sideNav.addItem(AdminConstants.getAdminNavigation()); + } + }); + return sideNav; + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/common/views/MainView.java b/springboot/src/main/java/de/thpeetz/kontor/common/views/MainView.java new file mode 100644 index 0000000..08edac9 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/common/views/MainView.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.common.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.flow.spring.annotation.SpringComponent; + +@SpringComponent +@Scope("prototype") +@AnonymousAllowed +@Route(value = "/", layout = MainLayout.class) +@PageTitle("Kontor") +public class MainView extends VerticalLayout { + +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/common/views/SeparateMainLayout.java b/springboot/src/main/java/de/thpeetz/kontor/common/views/SeparateMainLayout.java new file mode 100644 index 0000000..474d4a7 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/common/views/SeparateMainLayout.java @@ -0,0 +1,31 @@ +package de.thpeetz.kontor.common.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.security.SecurityService; + +public class SeparateMainLayout extends AppLayout { + + private final AdminService adminService; + + private final SecurityService securityService; + + public SeparateMainLayout(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + + KontorLayoutUtil layout = new KontorLayoutUtil(this, adminService, securityService); + layout.setSecondaryNavigation(getSecondaryNavigation()); + layout.createHeader("Kontor"); + } + + private HorizontalLayout getSecondaryNavigation() { + HorizontalLayout navigation = new HorizontalLayout(); + navigation.addClassNames(LumoUtility.JustifyContent.CENTER, LumoUtility.Gap.SMALL, LumoUtility.Height.MEDIUM); + return navigation; + } + +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/mailclient/data/Mail.java b/springboot/src/main/java/de/thpeetz/kontor/mailclient/data/Mail.java new file mode 100644 index 0000000..f3b80a8 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/mailclient/data/Mail.java @@ -0,0 +1,20 @@ +package de.thpeetz.kontor.mailclient.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +@Data +@EqualsAndHashCode(callSuper=false) +@Entity +public class Mail extends AbstractEntity { + + private String folder; + private String subject; + private String body; + private Date sentDate; + private Date receivedDate; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/mailclient/data/MailAccount.java b/springboot/src/main/java/de/thpeetz/kontor/mailclient/data/MailAccount.java new file mode 100644 index 0000000..840d8da --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/mailclient/data/MailAccount.java @@ -0,0 +1,29 @@ +package de.thpeetz.kontor.mailclient.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +@Setter +@Entity +public class MailAccount extends AbstractEntity { + + @NotEmpty + private String host; + + private Integer port; + + @NotEmpty + private String protocol; + + private String userName; + + private String password; + + private Boolean startTls; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/mailclient/views/EmailView.java b/springboot/src/main/java/de/thpeetz/kontor/mailclient/views/EmailView.java new file mode 100644 index 0000000..4235dd1 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/mailclient/views/EmailView.java @@ -0,0 +1,222 @@ +package de.thpeetz.kontor.mailclient.views; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Properties; + +import javax.mail.Address; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.NoSuchProviderException; +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.internet.InternetAddress; + +import com.sun.mail.imap.IMAPFolder; +import com.vaadin.flow.component.Composite; +import com.vaadin.flow.component.accordion.Accordion; +import com.vaadin.flow.component.html.Paragraph; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.menubar.MenuBar; +import com.vaadin.flow.component.messages.MessageList; +import com.vaadin.flow.component.messages.MessageListItem; +import com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment; +import com.vaadin.flow.component.orderedlayout.FlexComponent.JustifyContentMode; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.sidenav.SideNav; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouteAlias; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.flow.theme.lumo.LumoUtility.Gap; + +import de.thpeetz.kontor.mailclient.data.MailAccount; +import de.thpeetz.kontor.admin.services.MailService; +import de.thpeetz.kontor.common.views.MainLayout; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@PageTitle("Email") +@AnonymousAllowed +@Route(value = "mail", layout = MainLayout.class) +@RouteAlias(value = "kontor/mail", layout = MainLayout.class) +public class EmailView extends Composite { + + MenuBar menuBar = new MenuBar(); + Accordion accordion = new Accordion(); + SideNav folderList = new SideNav(); + MailService mailService; + + public EmailView(MailService mailService) { + this.mailService = mailService; + configureView(); + updateFolderList(); + } + + private void updateFolderList() { + List accounts = mailService.findAllMailAccounts(); + MailAccount account = accounts.get(0); + + Properties properties = new Properties(); + properties.put(String.format("mail.%s.host", account.getProtocol()), account.getHost()); + properties.put(String.format("mail.%s.port", account.getProtocol()), account.getPort().toString()); + properties.put("mail.imap.starttls.enable", "true"); + + Session session = Session.getDefaultInstance(properties); + Store store; + try { + store = session.getStore(account.getProtocol()); + store.connect(account.getUserName(), account.getPassword()); + Folder rootFolder = store.getDefaultFolder(); + Folder[] folders = rootFolder.list(); + for (Folder value : folders) { + SideNavItem folder = addSubFolder(value); + folderList.addItem(folder); + } + log.info("{} folders found", folders.length); + Folder folder = store.getFolder("INBOX"); + IMAPFolder imapFolder = (IMAPFolder) folder; + imapFolder.open(Folder.READ_ONLY); + Message[] messages = imapFolder.getMessages(); + log.info("Folder {} opened", folder.getFullName()); + int messageCount = folder.getMessageCount(); + log.info("{} messages found", messages.length); + for (Message message : messages) { + log.info("Message {}: {}, {}, {}", message.getMessageNumber(), message.getSubject(), message.getReceivedDate(), message.getSentDate()); + //log.info("Message: {}", message.getAllRecipients()); + Address[] addresses; + printAddress(message.getRecipients(Message.RecipientType.TO)); + printAddress(message.getRecipients(Message.RecipientType.CC)); + printAddress(message.getRecipients(Message.RecipientType.BCC)); + } + store.close(); + log.info("Connection closed"); + } catch (NoSuchProviderException e) { + throw new RuntimeException(e); + } catch (MessagingException e) { + throw new RuntimeException(e); + } + } + + private void printAddress(Address[] addresses) { + if (addresses != null) { + for (Address address: addresses) { + log.info("Address {}: {}", address.getType(), address.toString()); + InternetAddress internetAddress = (InternetAddress)address; + log.info("InternetAddress {}: {}", internetAddress.getPersonal(), internetAddress.getAddress()); + } + } + } + + private SideNavItem addSubFolder(Folder mailFolder) throws MessagingException { + log.debug("addSubFolder: {}", mailFolder.getName()); + Span counter = new Span(String.valueOf(mailFolder.getMessageCount())); + counter.getElement().getThemeList().add("badge contrast pill"); + counter.getElement().setAttribute("aria-label", String.format("%d unread messages", mailFolder.getMessageCount())); + SideNavItem folder = new SideNavItem(mailFolder.getName()); + folder.setSuffixComponent(counter); + Folder[] folders = mailFolder.list(); + log.info("{} subfolders found", folders.length); + for (Folder value : folders) { + SideNavItem subFolder = addSubFolder(value); + folder.addItem(subFolder); + } + return folder; + } + + private void configureView() { + HorizontalLayout layoutMenubar = new HorizontalLayout(); + HorizontalLayout layoutRow2 = new HorizontalLayout(); + VerticalLayout layoutColumn2 = new VerticalLayout(); + VerticalLayout layoutColumn3 = new VerticalLayout(); + MessageList messageList = new MessageList(); + VerticalLayout layoutColumn4 = new VerticalLayout(); + Paragraph textMedium = new Paragraph(); + getContent().setWidth("100%"); + getContent().getStyle().set("flex-grow", "1"); + layoutMenubar.addClassName(Gap.MEDIUM); + layoutMenubar.setWidth("100%"); + layoutMenubar.setHeight("min-content"); + menuBar.setWidth("min-content"); + setMenuBarSampleData(menuBar); + layoutRow2.addClassName(Gap.MEDIUM); + layoutRow2.setWidth("100%"); + layoutRow2.getStyle().set("flex-grow", "1"); + layoutColumn2.getStyle().set("flex-grow", "1"); + accordion.setWidth("100%"); + setAccordionSampleData(accordion); + layoutColumn3.setHeightFull(); + layoutRow2.setFlexGrow(1.0, layoutColumn3); + layoutColumn3.setWidth("100%"); + layoutColumn3.getStyle().set("flex-grow", "1"); + layoutColumn3.setJustifyContentMode(JustifyContentMode.START); + layoutColumn3.setAlignItems(Alignment.START); + messageList.setWidth("100%"); + setMessageListSampleData(messageList); + layoutColumn4.setWidthFull(); + layoutColumn3.setFlexGrow(1.0, layoutColumn4); + layoutColumn4.setWidth("100%"); + layoutColumn4.getStyle().set("flex-grow", "1"); + textMedium.setText( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."); + textMedium.setWidth("100%"); + textMedium.getStyle().set("font-size", "var(--lumo-font-size-m)"); + getContent().add(layoutMenubar); + layoutMenubar.add(menuBar); + getContent().add(layoutRow2); + layoutRow2.add(layoutColumn2); + layoutColumn2.add(folderList); + layoutRow2.add(layoutColumn3); + layoutColumn3.add(messageList); + layoutColumn3.add(layoutColumn4); + layoutColumn4.add(textMedium); + } + + private void setMenuBarSampleData(MenuBar menuBar) { + menuBar.addItem("View"); + menuBar.addItem("Edit"); + menuBar.addItem("Share"); + menuBar.addItem("Move"); + } + + private void setAccordionSampleData(Accordion accordion) { + Span name = new Span("Sophia Williams"); + Span email = new Span("sophia.williams@company.com"); + Span phone = new Span("(501) 555-9128"); + VerticalLayout personalInformationLayout = new VerticalLayout(name, email, phone); + personalInformationLayout.setSpacing(false); + personalInformationLayout.setPadding(false); + accordion.add("Personal information", personalInformationLayout); + Span street = new Span("4027 Amber Lake Canyon"); + Span zipCode = new Span("72333-5884 Cozy Nook"); + Span city = new Span("Arkansas"); + VerticalLayout billingAddressLayout = new VerticalLayout(); + billingAddressLayout.setSpacing(false); + billingAddressLayout.setPadding(false); + billingAddressLayout.add(street, zipCode, city); + accordion.add("Billing address", billingAddressLayout); + Span cardBrand = new Span("Mastercard"); + Span cardNumber = new Span("1234 5678 9012 3456"); + Span expiryDate = new Span("Expires 06/21"); + VerticalLayout paymentLayout = new VerticalLayout(); + paymentLayout.setSpacing(false); + paymentLayout.setPadding(false); + paymentLayout.add(cardBrand, cardNumber, expiryDate); + accordion.add("Payment", paymentLayout); + } + + private void setMessageListSampleData(MessageList messageList) { + MessageListItem message1 = new MessageListItem("Nature does not hurry, yet everything gets accomplished.", + LocalDateTime.now().minusDays(1).toInstant(ZoneOffset.UTC), "Matt Mambo"); + message1.setUserColorIndex(1); + MessageListItem message2 = new MessageListItem( + "Using your talent, hobby or profession in a way that makes you contribute with something good to this world is truly the way to go.", + LocalDateTime.now().minusMinutes(55).toInstant(ZoneOffset.UTC), "Linsey Listy"); + message2.setUserColorIndex(2); + messageList.setItems(message1, message2); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/MediaConstants.java b/springboot/src/main/java/de/thpeetz/kontor/media/MediaConstants.java new file mode 100644 index 0000000..866f7b3 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/MediaConstants.java @@ -0,0 +1,35 @@ +package de.thpeetz.kontor.media; + +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.sidenav.SideNavItem; +import de.thpeetz.kontor.media.views.MediaArticleView; +import de.thpeetz.kontor.media.views.MediaFileView; +import de.thpeetz.kontor.media.views.MediaVideoView; + +import java.util.ArrayList; + +public class MediaConstants { + + public static final String MEDIA = "Media"; + public static final String MEDIAFILE_ROUTE = "media/mediafile"; + public static final String MEDIAVIDEO_ROUTE = "media/mediavideo"; + public static final String MEDIAARTICLE_ROUTE = "media/mediaarticle"; + public static final String MEDIA_ROLE = "ROLE_MEDIA"; + private static final String MEDIAFILE = "Media Files"; + private static final String MEDIAVIDEO = "Media Videos"; + private static final String MEDIAARTICLE = "Media Article"; + + public static SideNavItem getMediaNavigation(ArrayList roles) { + SideNavItem media = new SideNavItem(MEDIA, MEDIAFILE_ROUTE, VaadinIcon.VIMEO.create()); + media.addItem(new SideNavItem(MEDIAVIDEO, MediaVideoView.class)); + media.addItem(new SideNavItem(MEDIAARTICLE, MediaArticleView.class)); + if (roles.contains(MEDIA_ROLE)) { + media.addItem(new SideNavItem(MEDIAFILE, MediaFileView.class)); + } + return media; + } + + private MediaConstants() { + // private constructor to hide the implicit public one + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/SetupModuleMedia.java b/springboot/src/main/java/de/thpeetz/kontor/media/SetupModuleMedia.java new file mode 100644 index 0000000..c9ce7a1 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/SetupModuleMedia.java @@ -0,0 +1,39 @@ +package de.thpeetz.kontor.media; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.admin.services.ModuleService; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class SetupModuleMedia implements ApplicationListener { + + boolean alreadySetup = false; + + @Autowired + private AdminService adminService; + + @Autowired + private ModuleService moduleService; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + adminService.addRole(MediaConstants.MEDIA_ROLE); + if (alreadySetup) { + log.info("SetupModuleMedia already executed, skipping"); + return; + } + if (!moduleService.importData(MediaConstants.MEDIA)) { + log.info("Module media should not setup data"); + return; + } + + log.info("Set up Media data"); + moduleService.setDataImported(MediaConstants.MEDIA); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaArticle.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaArticle.java new file mode 100644 index 0000000..c664a3a --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaArticle.java @@ -0,0 +1,29 @@ +package de.thpeetz.kontor.media.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +@Setter +@Entity +@Table(indexes = @Index(columnList = "url"), uniqueConstraints = @UniqueConstraint(columnNames = {"url"})) +public class MediaArticle extends AbstractEntity { + + @NotEmpty + private String url; + + private boolean review; + + @Nullable + private String title; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaArticleRepository.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaArticleRepository.java new file mode 100644 index 0000000..a25702e --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaArticleRepository.java @@ -0,0 +1,13 @@ +package de.thpeetz.kontor.media.data; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface MediaArticleRepository extends JpaRepository { + @Query("select m from MediaArticle m " + + "where lower(m.url) like lower(concat('%', :searchTerm, '%')) or lower(m.title) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java new file mode 100644 index 0000000..fa7b934 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java @@ -0,0 +1,40 @@ +package de.thpeetz.kontor.media.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +@Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "url" }) }) +public class MediaFile extends AbstractEntity { + + @Nullable + private String url; + + private boolean review; + + private boolean shouldDownload; + + @Nullable + private String title; + + @Nullable + private String cloudLink; + + @Nullable + private String fileName; + + @Nullable + private String path; + +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFileRepository.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFileRepository.java new file mode 100644 index 0000000..7db70e1 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFileRepository.java @@ -0,0 +1,14 @@ +package de.thpeetz.kontor.media.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface MediaFileRepository extends JpaRepository { + @Query("select m from MediaFile m " + + "where lower(m.url) like lower(concat('%', :searchTerm, '%')) or lower(m.title) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); + +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaLink.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaLink.java new file mode 100644 index 0000000..c70db48 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaLink.java @@ -0,0 +1,40 @@ +package de.thpeetz.kontor.media.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + + +@Slf4j +@Getter +@Setter +@Entity +@Table(indexes = @Index(columnList = "url"), uniqueConstraints = @UniqueConstraint(columnNames = {"url"})) +public class MediaLink extends AbstractEntity { + + @NotEmpty + private String url; + + private String title; + + private boolean review; + + @OneToOne + @JoinColumn(name = "video_id", referencedColumnName = "id") + @Nullable + private MediaVideo mediaVideo; + + @OneToOne + @JoinColumn(name = "article_id", referencedColumnName = "id") + @Nullable + private MediaArticle mediaArticle; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaVideo.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaVideo.java new file mode 100644 index 0000000..d093eb1 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaVideo.java @@ -0,0 +1,40 @@ +package de.thpeetz.kontor.media.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +@Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "url" }) }) +public class MediaVideo extends AbstractEntity { + + @Nullable + private String url; + + private boolean review; + + private boolean shouldDownload; + + @Nullable + private String title; + + @Nullable + private String cloudLink; + + @Nullable + private String fileName; + + @Nullable + private String path; + +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaVideoRepository.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaVideoRepository.java new file mode 100644 index 0000000..d898186 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaVideoRepository.java @@ -0,0 +1,15 @@ +package de.thpeetz.kontor.media.data; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface MediaVideoRepository extends JpaRepository { + + @Query("select m from MediaVideo m " + + "where lower(m.url) like lower(concat('%', :searchTerm, '%')) or lower(m.title) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); + +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaArticleService.java b/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaArticleService.java new file mode 100644 index 0000000..fc78d5d --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaArticleService.java @@ -0,0 +1,42 @@ +package de.thpeetz.kontor.media.services; + +import de.thpeetz.kontor.media.data.MediaArticle; +import de.thpeetz.kontor.media.data.MediaArticleRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +public class MediaArticleService { + + private final MediaArticleRepository mediaArticleRepository; + + public MediaArticleService(MediaArticleRepository mediaArticleRepository) { + this.mediaArticleRepository = mediaArticleRepository; + } + + public List findAllMediaArticles(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + log.info("Found {} entries", mediaArticleRepository.count()); + return mediaArticleRepository.findAll(); + } else { + List results = mediaArticleRepository.search(stringFilter); + log.info("Found {} entries", results.size()); + return results; + } + } + + public void saveMediaArticle(MediaArticle mediaArticle) { + if (mediaArticle == null) { + log.warn("MediaArticle is null. Are you sure you have connected your form to the application?"); + return; + } + mediaArticleRepository.save(mediaArticle); + } + + public void deleteMediaArticle(MediaArticle mediaArticle) { + mediaArticleRepository.delete(mediaArticle); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java b/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java new file mode 100644 index 0000000..a7fe0a1 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java @@ -0,0 +1,42 @@ +package de.thpeetz.kontor.media.services; + +import de.thpeetz.kontor.media.data.MediaFile; +import de.thpeetz.kontor.media.data.MediaFileRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +public class MediaFileService { + + private final MediaFileRepository mediaFileRepository; + + public MediaFileService(MediaFileRepository mediaFileRepository) { + this.mediaFileRepository = mediaFileRepository; + } + + public List findAllMediaFiles(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + log.info("Found " + mediaFileRepository.count()+ " entries"); + return mediaFileRepository.findAll(); + } else { + List results = mediaFileRepository.search(stringFilter); + log.info("Found " + results.size() + " entries"); + return results; + } + } + + public void saveMediaFile(MediaFile mediaFile) { + if (mediaFile == null) { + log.warn("MediaFile is null. Are you sure you have connected your form to the application?"); + return; + } + mediaFileRepository.save(mediaFile); + } + + public void deleteMediaFile(MediaFile mediaFile) { + mediaFileRepository.delete(mediaFile); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaVideoService.java b/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaVideoService.java new file mode 100644 index 0000000..61299fd --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaVideoService.java @@ -0,0 +1,39 @@ +package de.thpeetz.kontor.media.services; + +import de.thpeetz.kontor.media.data.MediaVideo; +import de.thpeetz.kontor.media.data.MediaVideoRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +public class MediaVideoService { + + private final MediaVideoRepository mediaVideoRepository; + + public MediaVideoService(MediaVideoRepository mediaVideoRepository) { + this.mediaVideoRepository = mediaVideoRepository; + } + + public List findAllMediaVideos(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return mediaVideoRepository.findAll(); + } else { + return mediaVideoRepository.search(stringFilter); + } + } + + public void saveMediaVideo(MediaVideo mediaVideo) { + if (mediaVideo == null) { + log.warn("MediaFile is null. Are you sure you have connected your form to the application?"); + return; + } + mediaVideoRepository.save(mediaVideo); + } + + public void deleteMediaVideo(MediaVideo mediaVideo) { + mediaVideoRepository.delete(mediaVideo); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaArticleForm.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaArticleForm.java new file mode 100644 index 0000000..d658e4e --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaArticleForm.java @@ -0,0 +1,108 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.media.data.MediaArticle; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MediaArticleForm extends FormLayout { + + TextField url = new TextField("URL"); + TextField title = new TextField("Title"); + Checkbox review = new Checkbox("Review"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(MediaArticle.class); + + public MediaArticleForm() { + addClassName("mediaarticle-form"); + binder.bindInstanceFields(this); + add(url, 2); + add(title, 2); + add(review, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new MediaArticleForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new MediaArticleForm.CloseEvent(this))); + + binder.addStatusChangeListener(event -> { + save.setEnabled(event.getBinder().isValid()); + }); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new MediaArticleForm.SaveEvent(this, binder.getBean())); + } + } + + public void setMediaArticle(MediaArticle mediaArticle) { + binder.setBean(mediaArticle); + } + + public abstract static class MediaArticleFormEvent extends ComponentEvent { + private MediaArticle mediaArticle; + + protected MediaArticleFormEvent(MediaArticleForm source, MediaArticle mediaArticle) { + super(source, false); + this.mediaArticle = mediaArticle; + } + + public MediaArticle getMediaArticle() { + return mediaArticle; + } + } + + public static class SaveEvent extends MediaArticleForm.MediaArticleFormEvent { + SaveEvent(MediaArticleForm source, MediaArticle mediaArticle) { + super(source, mediaArticle); + } + } + + public static class DeleteEvent extends MediaArticleForm.MediaArticleFormEvent { + DeleteEvent(MediaArticleForm source, MediaArticle mediaArticle) { + super(source, mediaArticle); + } + } + + public static class CloseEvent extends MediaArticleForm.MediaArticleFormEvent { + CloseEvent(MediaArticleForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(MediaArticleForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(MediaArticleForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(MediaArticleForm.CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaArticleView.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaArticleView.java new file mode 100644 index 0000000..67079d4 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaArticleView.java @@ -0,0 +1,171 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.contextmenu.ContextMenu; +import com.vaadin.flow.component.contextmenu.MenuItem; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.media.MediaConstants; +import de.thpeetz.kontor.media.data.MediaArticle; +import de.thpeetz.kontor.media.services.MediaArticleService; +import jakarta.annotation.security.PermitAll; +import lombok.Getter; +import org.springframework.context.annotation.Scope; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = MediaConstants.MEDIAARTICLE_ROUTE, layout = MainLayout.class) +@PageTitle("MediaArticle | Media | Kontor") +public class MediaArticleView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(MediaArticle.class, false); + Grid.Column idColumn = grid.addColumn(MediaArticle::getId).setHeader("ID").setResizable(true); + Grid.Column urlColumn = grid.addColumn(MediaArticle::getUrl).setHeader("URL").setResizable(true).setSortable(true); + Grid.Column titleColumn = grid.addColumn(MediaArticle::getTitle).setHeader("Titel").setResizable(true).setSortable(true); + Grid.Column reviewColumn = grid.addComponentColumn(mediaArticle -> createStatusIcon(mediaArticle.isReview())) + .setHeader("Überprüfung") + .setWidth("6rem"); + TextField searchField = new TextField(); + @Getter + MediaArticleForm form; + MediaArticleService service; + + private static class ColumnToggleContextMenu extends ContextMenu { + public ColumnToggleContextMenu(Component target) { + super(target); + setOpenOnClick(true); + } + + void addColumnToggleItem(String label, Grid.Column column) { + MenuItem menuItem = this.addItem(label, e -> { + column.setVisible(e.getSource().isChecked()); + }); + menuItem.setCheckable(true); + menuItem.setChecked(column.isVisible()); + menuItem.setKeepOpen(true); + } + } + + public MediaArticleView(MediaArticleService service) { + this.service = service; + addClassName("mediaarticle-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("mediaarticle-grid"); + grid.setSizeFull(); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + idColumn.setVisible(false); + grid.asSingleSelect().addValueChangeListener(event -> editMediaArticle(event.getValue())); + } + + private void configureForm() { + form = new MediaArticleForm(); + form.setWidth("75em"); + form.setVisible(false); + form.addSaveListener(this::saveMediaArticle); + form.addDeleteListener(this::deleteMediaArticle); + form.addCloseListener(e -> closeEditor()); + } + + private void saveMediaArticle(MediaArticleForm.SaveEvent event) { + service.saveMediaArticle(event.getMediaArticle()); + updateList(); + closeEditor(); + } + + private Icon createStatusIcon(boolean status) { + Icon icon; + if (status) { + icon = VaadinIcon.CHECK.create(); + icon.getElement().getThemeList().add("badge success"); + } else { + icon = VaadinIcon.CLOSE_SMALL.create(); + icon.getElement().getThemeList().add("badge error"); + } + icon.getStyle().set("padding", "var(--lumo-space-xs"); + return icon; + } + + private void deleteMediaArticle(MediaArticleForm.DeleteEvent event) { + service.deleteMediaArticle(event.getMediaArticle()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + searchField.setPlaceholder("Search"); + searchField.setClearButtonVisible(true); + searchField.setPrefixComponent(new Icon(VaadinIcon.SEARCH)); + searchField.setValueChangeMode(ValueChangeMode.EAGER); + searchField.addValueChangeListener(e -> updateList()); + + Button addMediaArticleButton = new Button("Add MediaArticle"); + addMediaArticleButton.addClickListener(click -> addMediaArticle()); + + Button menuButton = new Button("Show/Hide Columns"); + menuButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + MediaArticleView.ColumnToggleContextMenu columnToggleContextMenu = new MediaArticleView.ColumnToggleContextMenu(menuButton); + columnToggleContextMenu.addColumnToggleItem("ID", idColumn); + columnToggleContextMenu.addColumnToggleItem("URL", urlColumn); + columnToggleContextMenu.addColumnToggleItem("Titel", titleColumn); + columnToggleContextMenu.addColumnToggleItem("Überprüfung", reviewColumn); + HorizontalLayout toolbar = new HorizontalLayout(searchField, addMediaArticleButton, menuButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editMediaArticle(MediaArticle mediaArticle) { + if (mediaArticle == null) { + closeEditor(); + } else { + form.setMediaArticle(mediaArticle); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setMediaArticle(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addMediaArticle() { + grid.asSingleSelect().clear(); + editMediaArticle(new MediaArticle()); + } + + public void updateList() { + grid.setItems(service.findAllMediaArticles(searchField.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileForm.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileForm.java new file mode 100644 index 0000000..b1042fa --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileForm.java @@ -0,0 +1,113 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.media.data.MediaFile; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MediaFileForm extends FormLayout { + + TextField id = new TextField("ID"); + TextField url = new TextField("URL"); + TextField title = new TextField("Title"); + TextField fileName = new TextField("Dateiname"); + TextField cloudLink = new TextField("Cloud Link"); + Checkbox review = new Checkbox("Review"); + Checkbox shouldDownload = new Checkbox("Download"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(MediaFile.class); + + public MediaFileForm() { + addClassName("mediafile-form"); + binder.bindInstanceFields(this); + id.setReadOnly(true); + add(id, 2); + add(url, 2); + add(title, 2); + add(fileName, 2); + add(cloudLink, 2); + add(review, shouldDownload, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(event -> save.setEnabled(event.getBinder().isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setMediaFile(MediaFile mediaFile) { + binder.setBean(mediaFile); + } + + @Getter + public abstract static class MediaFileFormEvent extends ComponentEvent { + private final MediaFile mediaFile; + + protected MediaFileFormEvent(MediaFileForm source, MediaFile mediaFile) { + super(source, false); + this.mediaFile = mediaFile; + } + + } + + public static class SaveEvent extends MediaFileFormEvent { + SaveEvent(MediaFileForm source, MediaFile mediaFile) { + super(source, mediaFile); + } + } + + public static class DeleteEvent extends MediaFileFormEvent { + DeleteEvent(MediaFileForm source, MediaFile mediaFile) { + super(source, mediaFile); + } + } + + public static class CloseEvent extends MediaFileFormEvent { + CloseEvent(MediaFileForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileView.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileView.java new file mode 100644 index 0000000..0784b8d --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileView.java @@ -0,0 +1,186 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.contextmenu.ContextMenu; +import com.vaadin.flow.component.contextmenu.MenuItem; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.media.data.MediaFile; +import de.thpeetz.kontor.media.services.MediaFileService; +import jakarta.annotation.security.RolesAllowed; +import lombok.Getter; +import org.springframework.context.annotation.Scope; + +@SpringComponent +@Scope("prototype") +@RolesAllowed("MEDIA") +@Route(value = "media/mediafile", layout = MainLayout.class) +@PageTitle("MediaFile | Media | Kontor") +public class MediaFileView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(MediaFile.class, false); + Grid.Column idColumn = grid.addColumn(MediaFile::getId) + .setHeader("ID").setResizable(true).setSortable(true); + Grid.Column createdColumn = grid.addColumn(MediaFile::getCreatedDate) + .setHeader("Erstellt").setResizable(true).setSortable(true); + Grid.Column modifiedColumn = grid.addColumn(MediaFile::getLastModifiedDate) + .setHeader("Geändert").setResizable(true).setSortable(true); + Grid.Column urlColumn = grid.addColumn(MediaFile::getUrl) + .setHeader("URL").setResizable(true).setSortable(true); + Grid.Column titleColumn = grid.addColumn(MediaFile::getTitle) + .setHeader("Titel").setResizable(true).setSortable(true); + Grid.Column fileNameColumn = grid.addColumn(MediaFile::getFileName) + .setHeader("Dateiname").setResizable(true).setSortable(true); + Grid.Column cloudLinkColumn = grid.addColumn(MediaFile::getCloudLink) + .setHeader("Cloud Link").setResizable(true).setSortable(true); + Grid.Column reviewColumn = grid.addComponentColumn(mediafile -> createStatusIcon(mediafile.isReview())). + setHeader("Überprüfung").setWidth("6rem").setSortable(true); + Grid.Column shouldDownloadColumn = grid.addComponentColumn(mediafile -> createStatusIcon(mediafile.isShouldDownload())). + setHeader("Download?").setWidth("6rem").setSortable(true); + TextField searchField = new TextField(); + @Getter + MediaFileForm form; + MediaFileService service; + + private static class ColumnToggleContextMenu extends ContextMenu { + public ColumnToggleContextMenu(Component target) { + super(target); + setOpenOnClick(true); + } + + void addColumnToggleItem(String label, Grid.Column column) { + MenuItem menuItem = this.addItem(label, e -> { + column.setVisible(e.getSource().isChecked()); + }); + menuItem.setCheckable(true); + menuItem.setChecked(column.isVisible()); + menuItem.setKeepOpen(true); + } + } + + public MediaFileView(MediaFileService service) { + this.service = service; + addClassName("mediafile-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("mediafile-grid"); + grid.setSizeFull(); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + //idColumn.setVisible(false); + grid.asSingleSelect().addValueChangeListener(event -> editMediaFile(event.getValue())); + } + + private void configureForm() { + form = new MediaFileForm(); + form.setWidth("75em"); + form.setVisible(false); + form.addSaveListener(this::saveMediaFile); + form.addDeleteListener(this::deleteMediaFile); + form.addCloseListener(e -> closeEditor()); + } + + private void saveMediaFile(MediaFileForm.SaveEvent event) { + service.saveMediaFile(event.getMediaFile()); + updateList(); + closeEditor(); + } + + private Icon createStatusIcon(boolean status) { + Icon icon; + if (status) { + icon = VaadinIcon.CHECK.create(); + icon.getElement().getThemeList().add("badge success"); + } else { + icon = VaadinIcon.CLOSE_SMALL.create(); + icon.getElement().getThemeList().add("badge error"); + } + icon.getStyle().set("padding", "var(--lumo-space-xs"); + return icon; + } + + private void deleteMediaFile(MediaFileForm.DeleteEvent event) { + service.deleteMediaFile(event.getMediaFile()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + searchField.setPlaceholder("Search"); + searchField.setClearButtonVisible(true); + searchField.setPrefixComponent(new Icon(VaadinIcon.SEARCH)); + searchField.setValueChangeMode(ValueChangeMode.EAGER); + searchField.addValueChangeListener(e -> updateList()); + + Button addMediaFileButton = new Button("Add MediaFile"); + addMediaFileButton.addClickListener(click -> addMediaFile()); + + Button menuButton = new Button("Show/Hide Columns"); + menuButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + ColumnToggleContextMenu columnToggleContextMenu = new ColumnToggleContextMenu(menuButton); + columnToggleContextMenu.addColumnToggleItem("ID", idColumn); + columnToggleContextMenu.addColumnToggleItem("Erstellt", createdColumn); + columnToggleContextMenu.addColumnToggleItem("Geändert", modifiedColumn); + columnToggleContextMenu.addColumnToggleItem("URL", urlColumn); + columnToggleContextMenu.addColumnToggleItem("Titel", titleColumn); + columnToggleContextMenu.addColumnToggleItem("Dateiname", fileNameColumn); + columnToggleContextMenu.addColumnToggleItem("Cloud Link", cloudLinkColumn); + columnToggleContextMenu.addColumnToggleItem("Überprüfung", reviewColumn); + columnToggleContextMenu.addColumnToggleItem("Download?", shouldDownloadColumn); + HorizontalLayout toolbar = new HorizontalLayout(searchField, addMediaFileButton, menuButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editMediaFile(MediaFile mediaFile) { + if (mediaFile == null) { + closeEditor(); + } else { + form.setMediaFile(mediaFile); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setMediaFile(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addMediaFile() { + grid.asSingleSelect().clear(); + editMediaFile(new MediaFile()); + } + + public void updateList() { + grid.setItems(service.findAllMediaFiles(searchField.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaVideoForm.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaVideoForm.java new file mode 100644 index 0000000..dbc8d47 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaVideoForm.java @@ -0,0 +1,113 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.media.data.MediaVideo; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MediaVideoForm extends FormLayout { + + TextField url = new TextField("URL"); + TextField title = new TextField("Title"); + TextField fileName = new TextField("Dateiname"); + TextField cloudLink = new TextField("Cloud Link"); + Checkbox review = new Checkbox("Review"); + Checkbox shouldDownload = new Checkbox("Download"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(MediaVideo.class); + + public MediaVideoForm() { + addClassName("mediavideo-form"); + binder.bindInstanceFields(this); + add(url, 2); + add(title, 2); + add(fileName, 2); + add(cloudLink, 2); + add(review, shouldDownload, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(event -> { + save.setEnabled(event.getBinder().isValid()); + }); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setMediaVideo(MediaVideo mediaVideo) { + binder.setBean(mediaVideo); + } + + public abstract static class MediaVideoFormEvent extends ComponentEvent { + private MediaVideo mediaVideo; + + protected MediaVideoFormEvent(MediaVideoForm source, MediaVideo mediaVideo) { + super(source, false); + this.mediaVideo = mediaVideo; + } + + public MediaVideo getMediaVideo() { + return mediaVideo; + } + } + + public static class SaveEvent extends MediaVideoFormEvent { + SaveEvent(MediaVideoForm source, MediaVideo mediaVideo) { + super(source, mediaVideo); + } + } + + public static class DeleteEvent extends MediaVideoFormEvent { + DeleteEvent(MediaVideoForm source, MediaVideo mediaVideo) { + super(source, mediaVideo); + } + } + + public static class CloseEvent extends MediaVideoFormEvent { + CloseEvent(MediaVideoForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaVideoView.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaVideoView.java new file mode 100644 index 0000000..8b0c131 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaVideoView.java @@ -0,0 +1,178 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.contextmenu.ContextMenu; +import com.vaadin.flow.component.contextmenu.MenuItem; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.media.MediaConstants; +import de.thpeetz.kontor.media.data.MediaVideo; +import de.thpeetz.kontor.media.services.MediaVideoService; +import jakarta.annotation.security.PermitAll; +import lombok.Getter; +import org.springframework.context.annotation.Scope; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = MediaConstants.MEDIAVIDEO_ROUTE, layout = MainLayout.class) +@PageTitle("MediaVideo | Media | Kontor") +public class MediaVideoView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(MediaVideo.class, false); + Grid.Column idColumn = grid.addColumn(MediaVideo::getId).setHeader("ID").setResizable(true); + Grid.Column urlColumn = grid.addColumn(MediaVideo::getUrl).setHeader("URL").setResizable(true).setSortable(true); + Grid.Column titleColumn = grid.addColumn(MediaVideo::getTitle).setHeader("Titel").setResizable(true).setSortable(true); + Grid.Column fileNameColumn = grid.addColumn(MediaVideo::getFileName).setHeader("Dateiname").setResizable(true).setSortable(true); + Grid.Column cloudLinkColumn = grid.addColumn(MediaVideo::getCloudLink).setHeader("Cloud Link").setResizable(true).setSortable(true); + Grid.Column reviewColumn = grid.addComponentColumn(mediaVideo -> createStatusIcon(mediaVideo.isReview())) + .setHeader("Überprüfung") + .setWidth("6rem"); + Grid.Column shouldDownloadColumn = grid.addComponentColumn(mediaVideo -> createStatusIcon(mediaVideo.isShouldDownload())) + .setHeader("Download?") + .setWidth("6rem"); + TextField searchField = new TextField(); + @Getter + MediaVideoForm form; + MediaVideoService service; + + private static class ColumnToggleContextMenu extends ContextMenu { + public ColumnToggleContextMenu(Component target) { + super(target); + setOpenOnClick(true); + } + + void addColumnToggleItem(String label, Grid.Column column) { + MenuItem menuItem = this.addItem(label, e -> { + column.setVisible(e.getSource().isChecked()); + }); + menuItem.setCheckable(true); + menuItem.setChecked(column.isVisible()); + menuItem.setKeepOpen(true); + } + } + + public MediaVideoView(MediaVideoService service) { + this.service = service; + addClassName("mediavideo-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("mediavideo-grid"); + grid.setSizeFull(); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + idColumn.setVisible(false); + grid.asSingleSelect().addValueChangeListener(event -> editMediaVideo(event.getValue())); + } + + private void configureForm() { + form = new MediaVideoForm(); + form.setWidth("75em"); + form.setVisible(false); + form.addSaveListener(this::saveMediaVideo); + form.addDeleteListener(this::deleteMediaVideo); + form.addCloseListener(e -> closeEditor()); + } + + private void saveMediaVideo(MediaVideoForm.SaveEvent event) { + service.saveMediaVideo(event.getMediaVideo()); + updateList(); + closeEditor(); + } + + private Icon createStatusIcon(boolean status) { + Icon icon; + if (status) { + icon = VaadinIcon.CHECK.create(); + icon.getElement().getThemeList().add("badge success"); + } else { + icon = VaadinIcon.CLOSE_SMALL.create(); + icon.getElement().getThemeList().add("badge error"); + } + icon.getStyle().set("padding", "var(--lumo-space-xs"); + return icon; + } + + private void deleteMediaVideo(MediaVideoForm.DeleteEvent event) { + service.deleteMediaVideo(event.getMediaVideo()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + searchField.setPlaceholder("Search"); + searchField.setClearButtonVisible(true); + searchField.setPrefixComponent(new Icon(VaadinIcon.SEARCH)); + searchField.setValueChangeMode(ValueChangeMode.EAGER); + searchField.addValueChangeListener(e -> updateList()); + + Button addMediaVideoButton = new Button("Add MediaVideo"); + addMediaVideoButton.addClickListener(click -> addMediaVideo()); + + Button menuButton = new Button("Show/Hide Columns"); + menuButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + MediaVideoView.ColumnToggleContextMenu columnToggleContextMenu = new MediaVideoView.ColumnToggleContextMenu(menuButton); + columnToggleContextMenu.addColumnToggleItem("ID", idColumn); + columnToggleContextMenu.addColumnToggleItem("URL", urlColumn); + columnToggleContextMenu.addColumnToggleItem("Titel", titleColumn); + columnToggleContextMenu.addColumnToggleItem("Dateiname", fileNameColumn); + columnToggleContextMenu.addColumnToggleItem("Cloud Link", cloudLinkColumn); + columnToggleContextMenu.addColumnToggleItem("Überprüfung", reviewColumn); + columnToggleContextMenu.addColumnToggleItem("Download?", shouldDownloadColumn); + HorizontalLayout toolbar = new HorizontalLayout(searchField, addMediaVideoButton, menuButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editMediaVideo(MediaVideo mediaVideo) { + if (mediaVideo == null) { + closeEditor(); + } else { + form.setMediaVideo(mediaVideo); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setMediaVideo(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addMediaVideo() { + grid.asSingleSelect().clear(); + editMediaVideo(new MediaVideo()); + } + + public void updateList() { + grid.setItems(service.findAllMediaVideos(searchField.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/security/SecurityConfig.java b/springboot/src/main/java/de/thpeetz/kontor/security/SecurityConfig.java new file mode 100644 index 0000000..ae77838 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/security/SecurityConfig.java @@ -0,0 +1,59 @@ +package de.thpeetz.kontor.security; + +import com.vaadin.flow.spring.security.VaadinWebSecurity; + +import de.thpeetz.kontor.admin.services.KontorUserDetailsService; +import de.thpeetz.kontor.admin.views.LoginView; + +import java.util.Base64; + +import javax.crypto.spec.SecretKeySpec; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +@EnableWebSecurity +@Configuration +public class SecurityConfig extends VaadinWebSecurity { + + public static final String LOGOUT_URL = "/"; + + @Value("${jwt.auth.secret}") + private String authSecret; + + @Autowired + private KontorUserDetailsService userDetailsService; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeHttpRequests(auth -> auth.requestMatchers( + AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/images/*.png")).permitAll()); + super.configure(http); + setLoginView(http, LoginView.class); + setStatelessAuthentication(http, new SecretKeySpec(Base64.getDecoder().decode(authSecret), JwsAlgorithms.HS256), + "de.thpeetz.kontor"); + } + + @Bean + public DaoAuthenticationProvider authenticationProvider() { + final DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService); + authProvider.setPasswordEncoder(encoder()); + return authProvider; + } + + @Bean + public PasswordEncoder encoder() { + return new BCryptPasswordEncoder(11); + } +} \ No newline at end of file diff --git a/springboot/src/main/java/de/thpeetz/kontor/security/SecurityService.java b/springboot/src/main/java/de/thpeetz/kontor/security/SecurityService.java new file mode 100644 index 0000000..4d17d31 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/security/SecurityService.java @@ -0,0 +1,105 @@ +package de.thpeetz.kontor.security; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.stereotype.Component; + +import com.vaadin.flow.component.UI; +import com.vaadin.flow.server.VaadinServletRequest; +import com.vaadin.flow.server.VaadinServletResponse; +import com.vaadin.flow.spring.security.AuthenticationContext; + +import de.thpeetz.kontor.admin.services.KontorUserDetailsService; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Component +@Service +public class SecurityService { + + private static final String LOGOUT_SUCCESS_URL = "/"; + + private final AuthenticationContext authenticationContext; + + @Autowired + private KontorUserDetailsService userService; + + public SecurityService(AuthenticationContext authenticationContext) { + this.authenticationContext = authenticationContext; + } + + public Optional getAuthenticatedUser() { + String name = SecurityContextHolder.getContext().getAuthentication().getName(); + log.info("getAuthenticatedUser: {}", name); + + return Optional.ofNullable(userService.loadUserByUsername(name)); + } + + /*public static UserDetails getAuthenticatedUser() { + SecurityContext context = SecurityContextHolder.getContext(); + final Authentication authentication = context.getAuthentication(); + log.info("Authentication: {}", authentication); + if (authentication == null) { + return null; + } + Object principal = authentication.getPrincipal(); + if (principal instanceof UserDetails) { + return (UserDetails) authentication.getPrincipal(); + } + return null; + }*/ + + public void logout() { + log.info("logout"); + authenticationContext.logout(); + clearCookies(); + + UI.getCurrent().getPage().setLocation(LOGOUT_SUCCESS_URL); + SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler(); + logoutHandler.logout(VaadinServletRequest.getCurrent().getHttpServletRequest(), null, null); + clearCookies(); + } + + public boolean isLoggedIn() { + log.info("User {}", getAuthenticatedUser()); + return getAuthenticatedUser().isPresent(); + } + + private static final String JWT_HEADER_AND_PAYLOAD_COOKIE_NAME = "jwt.headerAndPayload"; + private static final String JWT_SIGNATURE_COOKIE_NAME = "jwt.signature"; + + private void clearCookies() { + log.info("clearCookies"); + clearCookie(JWT_HEADER_AND_PAYLOAD_COOKIE_NAME); + clearCookie(JWT_SIGNATURE_COOKIE_NAME); + } + + private void clearCookie(String cookieName) { + log.info("clearCookie: {}", cookieName); + HttpServletRequest request = VaadinServletRequest.getCurrent() + .getHttpServletRequest(); + HttpServletResponse response = VaadinServletResponse.getCurrent() + .getHttpServletResponse(); + + Cookie k = new Cookie(cookieName, null); + k.setPath(getRequestContextPath(request)); + k.setMaxAge(0); + k.setSecure(request.isSecure()); + k.setHttpOnly(false); + response.addCookie(k); + } + + private String getRequestContextPath(HttpServletRequest request) { + log.info("getRequestContextPath"); + final String contextPath = request.getContextPath(); + return "".equals(contextPath) ? "/" : contextPath; + } +} \ No newline at end of file diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/SetupModuleTysc.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/SetupModuleTysc.java new file mode 100644 index 0000000..bf6e1e4 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/SetupModuleTysc.java @@ -0,0 +1,519 @@ +package de.thpeetz.kontor.tysc; + +import de.thpeetz.kontor.tysc.data.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +import de.thpeetz.kontor.admin.services.ModuleService; +import de.thpeetz.kontor.tysc.data.FieldPosition; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class SetupModuleTysc implements ApplicationListener { + + boolean alreadySetup = false; + + @Autowired + private SportRepository sportRepository; + + @Autowired + private TeamRepository teamRepository; + + @Autowired + private VendorRepository vendorRepository; + + @Autowired + private FieldPositionRepository fieldPositionRepository; + + @Autowired + private PlayerRepository playerRepository; + + @Autowired + private CardSetRepository cardSetRepository; + + @Autowired + private RoosterRepository roosterRepository; + + @Autowired + private CardRepository cardRepository; + + @Autowired + private ModuleService moduleService; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + if (alreadySetup) { + log.info("SetupDataLoader(TYSC) already executed, skipping"); + return; + } + if (!moduleService.importData(TyscConstants.TYSC)) { + log.info("Module TradeYourSportsCards should not setup data"); + return; + } + log.info("Setp up TYSC data"); + Sport football = createSportIfNotFound("Football"); + Sport baseball = createSportIfNotFound("Baseball"); + Sport basketball = createSportIfNotFound("Basketball"); + Sport hockey = createSportIfNotFound("Hockey"); + createTeamIfNotFound(football, "Buffalo Bills", "Bills"); + Team colts = createTeamIfNotFound(football, "Indianapolis Colts", "Colts"); + createTeamIfNotFound(football, "Miami Dolphins", "Dolphins"); + Team patriots = createTeamIfNotFound(football, "New England Patriots", "Patriots"); + createTeamIfNotFound(football, "New York Jets", "Jets"); + Team ravens = createTeamIfNotFound(football, "Baltimore Ravens", "Ravens"); + createTeamIfNotFound(football, "Cincinnati Bengals", "Bengals"); + Team browns = createTeamIfNotFound(football, "Cleveland Browns", "Browns"); + createTeamIfNotFound(football, "Jacksonville Jaguars", "Jaguars"); + Team steelers = createTeamIfNotFound(football, "Pittsburgh Steelers", "Steelers"); + createTeamIfNotFound(football, "Tennessee Titans", "Titans"); + createTeamIfNotFound(football, "Denver Broncos", "Broncos"); + createTeamIfNotFound(football, "Kansas City Chiefs", "Chiefs"); + createTeamIfNotFound(football, "Oakland Raiders", "Raiders"); + createTeamIfNotFound(football, "San Diego Chargers", "Chargers"); + createTeamIfNotFound(football, "Seattle Seahawks", "Seahawks"); + createTeamIfNotFound(football, "Arizona Cardinals", "Cardinals"); + createTeamIfNotFound(football, "Dallas Cowboys", "Cowboys"); + createTeamIfNotFound(football, "New York Giants", "Giants"); + createTeamIfNotFound(football, "Philadelphia Eagles", "Eagles"); + Team redskins = createTeamIfNotFound(football, "Washington Redskins", "Redskins"); + createTeamIfNotFound(football, "Chicago Bears", "Bears"); + createTeamIfNotFound(football, "Detroit Lions", "Lions"); + createTeamIfNotFound(football, "Green Bay Packers", "Packers"); + createTeamIfNotFound(football, "Minnesota Vikings", "Vikings"); + createTeamIfNotFound(football, "Tampa Bay Buccaneers", "Buccaneers"); + createTeamIfNotFound(football, "Atlanta Falcons", "Falcons"); + createTeamIfNotFound(football, "Carolina Panthers", "Panthers"); + createTeamIfNotFound(football, "New Orleans Saints", "Saints"); + createTeamIfNotFound(football, "St.Louis Rams", "Rams"); + createTeamIfNotFound(football, "San Francisco 49ers", "49ers"); + createTeamIfNotFound(football, "Houston Texans", "Texans"); + createTeamIfNotFound(football, "Houston Oilers", "Oilers"); + createTeamIfNotFound(baseball, "Baltimore Orioles", "Orioles"); + createTeamIfNotFound(baseball, "Boston Red Sox", "Red Sox"); + createTeamIfNotFound(baseball, "New York Yankees", "Yankees"); + createTeamIfNotFound(baseball, "Tampa Bay Devil Rays", "Devil Rays"); + createTeamIfNotFound(baseball, "Toronto Blue Jays", "Blue Jays"); + createTeamIfNotFound(baseball, "Chicago White Sox", "White Sox"); + createTeamIfNotFound(baseball, "Cleveland Indians", "Indians"); + createTeamIfNotFound(baseball, "Detroit Tigers", "Tigers"); + createTeamIfNotFound(baseball, "Kansas City Royals", "Royals"); + createTeamIfNotFound(baseball, "Minnesota Twins", "Twins"); + createTeamIfNotFound(baseball, "Anaheim Angels", "Angels"); + createTeamIfNotFound(baseball, "Oakland Athletics", "Athletics"); + createTeamIfNotFound(baseball, "Seattle Mariners", "Mariners"); + createTeamIfNotFound(baseball, "Texas Rangers", "Rangers"); + createTeamIfNotFound(baseball, "Atlanta Braves", "Braves"); + createTeamIfNotFound(baseball, "Florida Marlins", "Marlins"); + createTeamIfNotFound(baseball, "Montreal Expos", "Expos"); + createTeamIfNotFound(baseball, "New York Mets", "Mets"); + createTeamIfNotFound(baseball, "Philadelphia Phillies", "Phillies"); + createTeamIfNotFound(baseball, "Chicago Cubs", "Cubs"); + createTeamIfNotFound(baseball, "Cincinnati Reds", "Reds"); + createTeamIfNotFound(baseball, "Houston Astros", "Astros"); + createTeamIfNotFound(baseball, "Milwaukee Brewers", "Brewers"); + createTeamIfNotFound(baseball, "Pittsburgh Pirates", "Pirates"); + createTeamIfNotFound(baseball, "St.Louis Cardinals", "Cardinals"); + createTeamIfNotFound(baseball, "Arizona Diamondbacks", "Diamondbacks"); + createTeamIfNotFound(baseball, "Colorado Rockies", "Rockies"); + createTeamIfNotFound(baseball, "Los Angeles Dodgers", "Dodgers"); + createTeamIfNotFound(baseball, "San Diego Padres", "Padres"); + createTeamIfNotFound(baseball, "San Francisco Giants", "Giants"); + createTeamIfNotFound(basketball, "Boston Celtics", "Celtics"); + createTeamIfNotFound(basketball, "Miami Heat", "Heat"); + createTeamIfNotFound(basketball, "New Jersey Nets", "Mets"); + createTeamIfNotFound(basketball, "New York Knicks", "Knicks"); + createTeamIfNotFound(basketball, "Orlando Magic", "Magic"); + createTeamIfNotFound(basketball, "Philadelphia 76ers", "76ers"); + createTeamIfNotFound(basketball, "Washington Wizards", "Wizards"); + createTeamIfNotFound(basketball, "Atlanta Hawks", "Hawks"); + createTeamIfNotFound(basketball, "Charlotte Hornets", "Hornets"); + createTeamIfNotFound(basketball, "Chicago Bulls", "Bulls"); + createTeamIfNotFound(basketball, "Cleveland Cavaliers", "Cavaliers"); + createTeamIfNotFound(basketball, "Detroit Pistons", "Pistons"); + createTeamIfNotFound(basketball, "Indiana Pacers", "Pacers"); + createTeamIfNotFound(basketball, "Milwaukee Bucks", "Bucks"); + createTeamIfNotFound(basketball, "Toronto Raptors", "Raptors"); + createTeamIfNotFound(basketball, "Dallas Mavericks", "Mavericks"); + createTeamIfNotFound(basketball, "Denver Nuggets", "Nuggets"); + createTeamIfNotFound(basketball, "Houston Rockets", "Rockets"); + createTeamIfNotFound(basketball, "Minnesota Timberwolves", "Timberwolves"); + createTeamIfNotFound(basketball, "San Antonio Spurs", "Spurs"); + createTeamIfNotFound(basketball, "Utah Jazz", "Jazz"); + createTeamIfNotFound(basketball, "Vancouver Grizzlies", "Grizzlies"); + createTeamIfNotFound(basketball, "Golden State Warriors", "Warriors"); + createTeamIfNotFound(basketball, "Los Angeles Clippers", "Clippers"); + createTeamIfNotFound(basketball, "Los Angeles Lakers", "Lakers"); + createTeamIfNotFound(basketball, "Phoenix Suns", "Suns"); + createTeamIfNotFound(basketball, "Portland Trail Blazers", "Blazers"); + createTeamIfNotFound(basketball, "Sacramento Kings", "Kings"); + createTeamIfNotFound(basketball, "Seattle SuperSonics", "SuperSonics"); + createTeamIfNotFound(hockey, "Boston Bruins", "Bruins"); + createTeamIfNotFound(hockey, "Buffalo Sabres", "Sabres"); + createTeamIfNotFound(hockey, "Montreal Canadiens", "Canadiens"); + createTeamIfNotFound(hockey, "Ottawa Senators", "Senators"); + createTeamIfNotFound(hockey, "Toronto Maple Leafs", "Maple Leafs"); + createTeamIfNotFound(hockey, "New Jersey Devils", "Devils"); + createTeamIfNotFound(hockey, "New York Islanders", "Islanders"); + createTeamIfNotFound(hockey, "New York Rangers", "Rangers"); + createTeamIfNotFound(hockey, "Philadelphia Flyers", "Flyers"); + createTeamIfNotFound(hockey, "Pittsburgh Penguins", "Penguins"); + createTeamIfNotFound(hockey, "Atlanta Trashers", "Trashers"); + createTeamIfNotFound(hockey, "Carolina Hurricanes", "Hurricanes"); + createTeamIfNotFound(hockey, "Florida Panthers", "Panthers"); + createTeamIfNotFound(hockey, "Tampa Bay Lightnings", "Lightnings"); + createTeamIfNotFound(hockey, "Washington Capitals", "Capitals"); + createTeamIfNotFound(hockey, "Chicago Blackhawks", "Blackhawks"); + createTeamIfNotFound(hockey, "Columbus Blue Jackets", "Blue Jackets"); + createTeamIfNotFound(hockey, "Detroit Red Wings", "Red Wings"); + createTeamIfNotFound(hockey, "Nashville Predators", "Predators"); + createTeamIfNotFound(hockey, "St.Louis Blues", "Blues"); + createTeamIfNotFound(hockey, "Calgary Flames", "Flames"); + createTeamIfNotFound(hockey, "Colorado Avalanche", "Avalanche"); + createTeamIfNotFound(hockey, "Edmonton Oilers", "Oilers"); + createTeamIfNotFound(hockey, "Minnesota Wild", "Wild"); + createTeamIfNotFound(hockey, "Vancouver Canucks", "Canucks"); + createTeamIfNotFound(hockey, "Anaheim Mighty Ducks", "Mighty Ducks"); + createTeamIfNotFound(hockey, "Dallas Stars", "Stars"); + createTeamIfNotFound(hockey, "Los Angeles Kings", "Kings"); + createTeamIfNotFound(hockey, "Phoenix Coyotes", "Coyotes"); + createTeamIfNotFound(hockey, "San Jose Sharks", "Sharks"); + FieldPosition qb = createPosition(football, "QB", "Quarterback"); + FieldPosition rb = createPosition(football, "RB", "Running Back"); + FieldPosition wr = createPosition(football, "WR", "Wide Receiver"); + FieldPosition te = createPosition(football, "TE", "Tight End"); + FieldPosition fb = createPosition(football, "FB", "Fullback"); + createPosition(football, "OL", "Offensive Line"); + createPosition(football, "DL", "Defensive Line"); + FieldPosition lb = createPosition(football, "LB", "Linebacker"); + createPosition(football, "DB", "Defensive Back"); + createPosition(football, "DE", "Defensive End"); + createPosition(football, "K", "Kicker"); + createPosition(football, "P", "Punter"); + createPosition(football, "S", "Safety"); + createPosition(football, "KR", "Kick Returner"); + createPosition(football, "PR", "Punt Returner"); + createPosition(football, "LS", "Long Snapper"); + createPosition(football, "LG", "Left Guard"); + createPosition(football, "RG", "Right Guard"); + createPosition(football, "OF", "Offensive Tackle"); + createPosition(football, "DB", "Defensive Back"); + createPosition(football, "CB", "Cornerback"); + createPosition(football, "DT", "Defensive Tackle"); + createPosition(football, "NT", "Nose Tackle"); + createPosition(football, "OLB", "Outside Linebacker"); + createPosition(football, "ILB", "Inside Linebacker"); + createPosition(football, "SS", "Strong Safety"); + createPosition(baseball, "P", "Pitcher"); + createPosition(baseball, "C", "Catcher"); + createPosition(baseball, "1B", "First Base"); + createPosition(baseball, "2B", "Second Base"); + createPosition(baseball, "3B", "Third Base"); + createPosition(baseball, "SS", "Shortstop"); + createPosition(baseball, "LF", "Left Field"); + createPosition(baseball, "CF", "Center Field"); + createPosition(baseball, "RF", "Right Field"); + createPosition(basketball, "PG", "Point Guard"); + createPosition(basketball, "SG", "Shooting Guard"); + createPosition(basketball, "SF", "Small Forward"); + createPosition(basketball, "PF", "Power Forward"); + createPosition(basketball, "C", "Center"); + createPosition(hockey, "G", "Goalie"); + createPosition(hockey, "D", "Defense"); + createPosition(hockey, "LW", "Left Wing"); + createPosition(hockey, "RW", "Right Wing"); + createPosition(hockey, "C", "Center"); + Vendor pacific = createVendorIfNotFound("Pacific"); + Vendor fleer = createVendorIfNotFound("Fleer"); + Vendor bowman = createVendorIfNotFound("Bowman"); + Vendor leaf = createVendorIfNotFound("Leaf"); + Vendor upperdeck = createVendorIfNotFound("Upper Deck"); + createVendorIfNotFound("Topps"); + createVendorIfNotFound("Donruss"); + createVendorIfNotFound("Score"); + createVendorIfNotFound("Flair"); + createCardSetIfNotFound("Mystique Big Buzz", fleer, false, true); + createCardSetIfNotFound("Mystique Gold", fleer, true, false); + createCardSetIfNotFound("Pacific Copper", pacific, true, false); + createCardSetIfNotFound("Pacific Gold", pacific, true, false); + CardSet pacificbase = createCardSetIfNotFound(pacific.getName(), pacific, false, false); + createCardSetIfNotFound(fleer.getName(), fleer, false, false); + createCardSetIfNotFound(bowman.getName(), bowman, false, false); + createCardSetIfNotFound(leaf.getName(), leaf, false, false); + createCardSetIfNotFound("Ultra", fleer, false, false); + createCardSetIfNotFound("Mystique", fleer, false, false); + createCardSetIfNotFound("Finest Hour", pacific, false, false); + createCardSetIfNotFound("SP", upperdeck, false, false); + createCardSetIfNotFound("SPX", upperdeck, false, false); + createCardSetIfNotFound("SP Authentic", upperdeck, false, false); + createCardSetIfNotFound("Black Diamond", upperdeck, false, false); + Player jeromepathon = createPlayerIfNotFound("Jerome", "Pathon"); + Player bruschi = createPlayerIfNotFound("Tedy", "Bruschi"); + Player couch = createPlayerIfNotFound("Tim", "Couch"); + Player shea = createPlayerIfNotFound("Aaron", "Shea"); + Player jamallewis = createPlayerIfNotFound("Jamal", "Lewis"); + Player jermainelewis = createPlayerIfNotFound("Jermaine", "Lewis"); + Player tonybanks = createPlayerIfNotFound("Tony", "Banks"); + Player chrisfuamatu = createPlayerIfNotFound("Chris", "Fuamatu-Ma'afala"); + Player jeromebettis = createPlayerIfNotFound("Jerome", "Bettis"); + Player kordellstewart = createPlayerIfNotFound("Kordell", "Stewart"); + createPlayerIfNotFound("Warren", "Moon"); + createPlayerIfNotFound("Kevin", "Lockett"); + createPlayerIfNotFound("Rich", "Gannon"); + createPlayerIfNotFound("James", "Jett"); + createPlayerIfNotFound("Mack", "Strong"); + createPlayerIfNotFound("Brock", "Huard"); + createPlayerIfNotFound("Ricky", "Watters"); + createPlayerIfNotFound("Troy", "Aikman"); + createPlayerIfNotFound("David", "LaFleur"); + createPlayerIfNotFound("Chris", "Brazzell"); + createPlayerIfNotFound("Ron", "Dayne"); + createPlayerIfNotFound("Na", "Brown"); + createPlayerIfNotFound("Torrance", "Small"); + createPlayerIfNotFound("Chad", "Lewis"); + createPlayerIfNotFound("Adrian", "Murrell"); + createPlayerIfNotFound("Chris", "Chandler"); + createPlayerIfNotFound("Danny", "Kanell"); + createPlayerIfNotFound("Ricky", "Williams"); + createPlayerIfNotFound("Jeff", "Garcia"); + createPlayerIfNotFound("Tai", "Streets"); + createPlayerIfNotFound("Charlie", "Garner"); + createPlayerIfNotFound("Drew", "Bledsoe"); + createPlayerIfNotFound("Antowain", "Smith"); + createPlayerIfNotFound("Terry", "Glenn"); + createPlayerIfNotFound("Jerry", "Rice"); + createPlayerIfNotFound("Terrell", "Owens"); + createPlayerIfNotFound("Isaac", "Bruce"); + createPlayerIfNotFound("Trung", "Canidate"); + Rooster pathoncoltswr2001 = createRoosterIfNotFound(jeromepathon, colts, wr, 2001); + Rooster bruschipatriotslb2001 = createRoosterIfNotFound(bruschi, patriots, lb, 2001); + Rooster couchbrownsqb2001 = createRoosterIfNotFound(couch, browns, qb, 2001); + Rooster sheabrownste2001 = createRoosterIfNotFound(shea, browns, te, 2001); + Rooster jamallewisravensrb2001 = createRoosterIfNotFound(jamallewis, ravens, rb, 2001); + Rooster jermainelewisravenswr2001 = createRoosterIfNotFound(jermainelewis, ravens, wr, 2001); + Rooster tonybanksravensqb2001 = createRoosterIfNotFound(tonybanks, ravens, qb, 2001); + createRoosterIfNotFound(tonybanks, redskins, qb, 2002); + Rooster chrisfuamatusteelersfb2001 = createRoosterIfNotFound(chrisfuamatu, steelers, fb, 2001); + Rooster jeromebettissteelersrb2001 = createRoosterIfNotFound(jeromebettis, steelers, rb, 2001); + Rooster kordellstewartsteelersqb2001 = createRoosterIfNotFound(kordellstewart, steelers, qb, 2001); + createCardIfNotFound(185, 2001, pacific, pacificbase, pathoncoltswr2001); + createCardIfNotFound(250, 2001, pacific, pacificbase, bruschipatriotslb2001); + createCardIfNotFound(103, 2001, pacific, pacificbase, couchbrownsqb2001); + createCardIfNotFound(112, 2001, pacific, pacificbase, sheabrownste2001); + createCardIfNotFound(37, 2001, pacific, pacificbase, jamallewisravensrb2001); + createCardIfNotFound(38, 2001, pacific, pacificbase, jermainelewisravenswr2001); + createCardIfNotFound(31, 2001, pacific, pacificbase, tonybanksravensqb2001); + createCardIfNotFound(338, 2001, pacific, pacificbase, chrisfuamatusteelersfb2001); + createCardIfNotFound(335, 2001, pacific, pacificbase, jeromebettissteelersrb2001); + createCardIfNotFound(345, 2001, pacific, pacificbase, kordellstewartsteelersqb2001); + /* + * INSERT INTO `spieler` (`ID`,`name`) VALUES + * (11,'Moon, Warren'), + * (12,'Lockett, Kevin'), + * (13,'Gannon, Rich'), + * (14,'Jett, James'), + * (15,'Strong, Mack'), + * (16,'Huard, Brock'), + * (17,'Watters, Ricky'), + * (18,'Aikman, Troy'), + * (19,'LaFleur, David'), + * (20,'Brazzell, Chris'), + * (21,'Dayne, Ron'), + * (22,'Brown, Na'), + * (23,'Small, Torrance'), + * (24,'Lewis, Chad'), + * (25,'Murrell, Adrian'), + * (26,'Smith, Maurice'), + * (27,'Chandler, Chris'), + * (28,'Kanell, Danny') + * ,(29,'Williams, Ricky'), + * (30,'Garcia, Jeff'), + * (31,'Streets, Tai'), + * (32,'Garner, Charlie'), + * (33,'Rice, Jerry'), + * (34,'Owens, Terrell'), + * (35,'Bruce, Isaac'), + * (36,'Canidate, Trung'); + * + * INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES + * (1,1,'Buffalo Bills','Bills'), + * (2,1,'Indianapolis Colts','Colts'), + * (3,1,'Miami Dolphins','Dolphins'), + * (4,1,'New England Patriots','Patriots'), + * (5,1,'New York Jets','Jets'), + * (6,1,'Baltimore Ravens','Ravens'), + * (7,1,'Cincinnati Bengals','Bengals'), + * (8,1,'Cleveland Browns','Browns'), + * (9,1,'Jacksonville Jaguars','Jaguars'), + * (10,1,'Pittsburgh Steelers','Steelers'), + * (11,1,'Tennessee Titans','Titans'), + * (12,1,'Denver Broncos','Broncos'), + * (13,1,'Kansas City Chiefs','Chiefs'), + * (14,1,'Oakland Raiders','Raiders'), + * (15,1,'San Diego Chargers','Chargers'), + * (16,1,'Seattle Seahawks','Seahawks'), + * (17,1,'Arizona Cardinals','Cardinals'), + * (18,1,'Dallas Cowboys','Cowboys'), + * (19,1,'New York Giants','Giants'), + * (20,1,'Philadelphia Eagles','Eagles'), + * (21,1,'Washington Redskins','Redskins'), + * (22,1,'Chicago Bears','Bears'), + * (23,1,'Detroit Lions','Lions'), + * (24,1,'Green Bay Packers','Packers'), + * (25,1,'Minnesota Vikings','Vikings'), + * (26,1,'Tampa Bay Buccaneers','Buccaneers'), + * (27,1,'Atlanta Falcons','Falcons'), + * (28,1,'Carolina Panthers','Panthers'); + * (29,1,'New Orleans Saints','Saints'), + * (30,1,'St.Louis Rams','Rams'), + * (31,1,'San Francisco 49ers','49ers'), + * + * INSERT INTO `karte` + * (`ID`,`spieler_id`,`team_id`,`hersteller_id`,`serie_id`,`parallel_id`,` + * inserts_id`,`rookie`,`jahr`,`nummer`) VALUES + * (11,11,13,1,1,0,0,'N',2001,213), + * (12,12,13,1,1,0,0,'N',2001,212), + * (13,13,14,1,1,0,0,'N',2001,311), + * (14,14,14,1,1,0,0,'N',2001,312), + * (15,15,16,1,1,0,0,'N',2001,403), + * (16,16,16,1,1,0,0,'N',2001,397), + * (17,17,16,1,1,0,0,'N',2001,404), + * (18,18,18,1,1,0,0,'N',2001,116), + * (19,19,18,1,1,0,0,'N',2001,122), + * (20,20,18,1,1,0,0,'N',2001,117), + * (21,21,19,1,1,0,0,'N',2001,281), + * (22,22,19,1,1,0,0,'N',2001,321), + * (23,23,20,1,1,0,0,'N',2001,331), + * (24,24,20,1,1,0,0,'N',2001,324), + * (25,25,21,1,1,0,0,'N',2001,445), + * (26,26,27,1,1,0,0,'N',2001,28), + * (27,27,27,1,1,0,0,'N',2001,17), + * (28,28,27,1,1,0,0,'N',2001,23), + * (29,29,29,1,1,0,0,'N',2001,273); + * (30,30,31,1,1,0,0,'N',2001,380), + * (31,31,31,1,1,0,0,'N',2001,390), + * (32,32,31,1,1,0,0,'N',2001,381), + * (33,33,31,1,1,0,0,'N',2001,387), + * (34,34,31,1,1,0,0,'N',2001,386), + * (35,35,30,1,1,0,0,'N',2001,349), + * (36,36,30,1,1,0,0,'N',2001,350), + * (37,37,44,5,8,0,0,'N',1994,106); + */ + + alreadySetup = true; + moduleService.setDataImported(TyscConstants.TYSC); + } + + private Sport createSportIfNotFound(String sport) { + log.info("createSportIfNotFound: {}", sport); + Sport sportEntity = sportRepository.findByName(sport); + if (sportEntity == null) { + log.info("Sport {} not found, will create it", sport); + sportEntity = new Sport(); + sportEntity.setName(sport); + sportRepository.save(sportEntity); + } + return sportEntity; + } + + private Team createTeamIfNotFound(Sport football, String name, String shortName) { + log.info("createTeamIfNotFound: {}", name); + Team team = teamRepository.findByName(name); + if (team == null) { + log.info("Team {} not found, will create it", name); + team = new Team(); + team.setName(name); + team.setShortName(shortName); + team.setSport(football); + teamRepository.save(team); + } + return team; + } + + private FieldPosition createPosition(Sport sport, String shortName, String name) { + log.info("createPosition: {} for Sport {}", name, sport.getName()); + FieldPosition fieldPosition = fieldPositionRepository.findByShortNameAndSport(shortName, sport); + if (fieldPosition == null) { + log.info("Position {} not found, will create it", name); + fieldPosition = new FieldPosition(); + fieldPosition.setShortName(shortName); + fieldPosition.setName(name); + fieldPosition.setSport(sport); + fieldPositionRepository.save(fieldPosition); + } + return fieldPosition; + } + + private Vendor createVendorIfNotFound(String name) { + log.info("createVendorIfNotFound: {}", name); + Vendor vendor = vendorRepository.findByName(name); + if (vendor == null) { + log.info("Vendor {} not found, will create it", name); + vendor = new Vendor(); + vendor.setName(name); + vendorRepository.save(vendor); + } + return vendor; + } + + private Player createPlayerIfNotFound(String firstName, String lastName) { + log.info("createPlayerIfNotFound: {} {}", firstName, lastName); + Player player = playerRepository.findByFirstNameAndLastName(firstName, lastName); + if (player == null) { + log.info("Player {} {} not found, will create it", firstName, lastName); + player = new Player(); + player.setFirstName(firstName); + player.setLastName(lastName); + playerRepository.save(player); + } + return player; + } + + private CardSet createCardSetIfNotFound(String setname, Vendor vendor, boolean parallelSet, boolean insertSet) { + log.info("createCardSetIfNotFound: {} {}", setname, vendor.getName()); + CardSet cardSet = cardSetRepository.findByNameAndVendor(setname, vendor); + if (cardSet == null) { + log.info("CardType {} not found, will create it", cardSet); + cardSet = new CardSet(); + cardSet.setName(setname); + cardSet.setVendor(vendor); + cardSet.setParallelSet(parallelSet); + cardSet.setInsertSet(insertSet); + cardSetRepository.save(cardSet); + } + return cardSet; + } + + private Rooster createRoosterIfNotFound(Player player, Team team, FieldPosition fieldPosition, int year) { + log.info("createRoosterIfNotFound; {} {} {} {}", player.getFirstName(), player.getLastName(), team.getName(), + year); + Rooster rooster = roosterRepository.findByReferences(player, team, fieldPosition, year); + if (rooster == null) { + log.info("Rooster {} not found, will create it", player); + rooster = new Rooster(); + rooster.setPlayer(player); + rooster.setTeam(team); + rooster.setPosition(fieldPosition); + rooster.setYear(year); + roosterRepository.save(rooster); + } + return rooster; + } + + private void createCardIfNotFound(int cardNumber, int year, Vendor vendor, CardSet cardset, Rooster rooster) { + log.info("createCardIfNotFound: vendor={} cardset={} rooster={} cardNumber={} year={}", vendor, cardset, + rooster, cardNumber, year); + Card card = cardRepository.search(vendor, cardset, rooster, cardNumber, year); + if (card == null) { + card = new Card(); + card.setVendor(vendor); + card.setCardSet(cardset); + card.setRooster(rooster); + card.setCardNumber(cardNumber); + card.setYear(year); + cardRepository.save(card); + } + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/TyscConstants.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/TyscConstants.java new file mode 100644 index 0000000..cc41c93 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/TyscConstants.java @@ -0,0 +1,88 @@ +package de.thpeetz.kontor.tysc; + +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.RouterLink; + +import de.thpeetz.kontor.tysc.views.CardSetView; +import de.thpeetz.kontor.tysc.views.CardView; +import de.thpeetz.kontor.tysc.views.PlayerView; +import de.thpeetz.kontor.tysc.views.PositionView; +import de.thpeetz.kontor.tysc.views.RoosterView; +import de.thpeetz.kontor.tysc.views.SportView; +import de.thpeetz.kontor.tysc.views.TeamView; +import de.thpeetz.kontor.tysc.views.VendorView; + +/** + * The {@code TyscConstants} class contains constant values related to tysc. + */ +public class TyscConstants { + + public static final String TYSC = "TradeYourSportsCards"; + public static final String TYSC_ROLE = "ROLE_TYSC"; + public static final String VENDOR = "Vendor"; + public static final String VENDOR_ROUTE = "tysc/vendor"; + public static final String CARDSET = "Card Set"; + public static final String CARDSET_ROUTE = "tysc/cardset"; + public static final String CARD = "Card"; + public static final String CARD_ROUTE = "tysc/card"; + public static final String SPORT = "Sport"; + public static final String SPORT_ROUTE = "tysc/sport"; + public static final String TEAM = "Team"; + public static final String TEAM_ROUTE = "tysc/team"; + public static final String POSITION = "Position"; + public static final String POSITION_ROUTE = "tysc/position"; + public static final String PLAYER = "Player"; + public static final String PLAYER_ROUTE = "tysc/player"; + public static final String ROOSTER = "Rooster"; + public static final String ROOSTER_ROUTE = "tysc/rooster"; + + public static RouterLink getSportLink() { + return new RouterLink(SPORT, SportView.class); + } + + public static RouterLink getTeamLink() { + return new RouterLink(TEAM, TeamView.class); + } + + public static RouterLink getPlayerLink() { + return new RouterLink(PLAYER, PlayerView.class); + } + + public static RouterLink getPositionLink() { + return new RouterLink(POSITION, PositionView.class); + } + + public static RouterLink getRoosterLink() { + return new RouterLink(ROOSTER, RoosterView.class); + } + + public static RouterLink getVendorLink() { + return new RouterLink(VENDOR, VendorView.class); + } + + public static RouterLink getCardSetLink() { + return new RouterLink(CARDSET, CardSetView.class); + } + + public static RouterLink getCardLink() { + return new RouterLink(CARD, CardView.class); + } + + public static SideNavItem getTyscNavigation() { + SideNavItem tysc = new SideNavItem(TYSC, VENDOR_ROUTE, VaadinIcon.ARCHIVE.create()); + tysc.addItem(new SideNavItem(SPORT, SportView.class)); + tysc.addItem(new SideNavItem(TEAM, TeamView.class)); + tysc.addItem(new SideNavItem(PLAYER, PlayerView.class)); + tysc.addItem(new SideNavItem(POSITION, PositionView.class)); + tysc.addItem(new SideNavItem(ROOSTER, RoosterView.class)); + tysc.addItem(new SideNavItem(CARDSET, CardSetView.class)); + tysc.addItem(new SideNavItem(CARD, CardView.class)); + tysc.addItem(new SideNavItem(VENDOR, VendorView.class)); + return tysc; + } + + private TyscConstants() { + // private constructor to hide the implicit public one + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Card.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Card.java new file mode 100644 index 0000000..90ec739 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Card.java @@ -0,0 +1,49 @@ +package de.thpeetz.kontor.tysc.data; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "cardNumber, year")}, + uniqueConstraints = {@UniqueConstraint(columnNames = { "cardNumber", "year", "vendor_id", "cardSet_id" })} +) +public class Card extends AbstractEntity { + + private int cardNumber; + + private int year; + + @ManyToOne + @JoinColumn(name = "vendor_id") + @NotNull + @JsonIgnoreProperties({ "cards" }) + private Vendor vendor; + + @ManyToOne + @JoinColumn(name = "cardSet_id") + @NotNull + @JsonIgnoreProperties({ "cards" }) + private CardSet cardSet; + + @ManyToOne + @JoinColumn(name = "rooster_id") + @NotNull + @JsonIgnoreProperties({ "cards" }) + private Rooster rooster; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardRepository.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardRepository.java new file mode 100644 index 0000000..c894b28 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface CardRepository extends JpaRepository { + + @Query("SELECT c FROM Card c WHERE c.vendor = ?1 AND c.cardSet = ?2 and c.rooster = ?3 and c.cardNumber = ?4 and c.year = ?5") + Card search(Vendor vendor, CardSet cardset, Rooster rooster, int cardNumber, int year); + + @Query("select c from Card c " + + "where str(c.cardNumber) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardSet.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardSet.java new file mode 100644 index 0000000..c72188c --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardSet.java @@ -0,0 +1,41 @@ +package de.thpeetz.kontor.tysc.data; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "name, vendor_id")}, + uniqueConstraints = {@UniqueConstraint(columnNames = { "name", "vendor_id" })} +) +public class CardSet extends AbstractEntity { + + @NotEmpty + private String name; + + @ManyToOne() + @JoinColumn(name = "vendor_id") + @NotNull + @JsonIgnoreProperties({ "cardSets" }) + private Vendor vendor; + + private boolean parallelSet = false; + + private boolean insertSet = false; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardSetRepository.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardSetRepository.java new file mode 100644 index 0000000..237953e --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/CardSetRepository.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface CardSetRepository extends JpaRepository { + + List findByName(String name); + + CardSet findByNameAndVendor(String name, Vendor vendor); + + @Query("select c from CardSet c " + + "where lower(c.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/FieldPosition.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/FieldPosition.java new file mode 100644 index 0000000..a806e70 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/FieldPosition.java @@ -0,0 +1,52 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "name, sport_id")}, + uniqueConstraints = { + @UniqueConstraint(columnNames = { "name", "sport_id" }), + @UniqueConstraint(columnNames = { "shortName", "sport_id" }) + } +) +public class FieldPosition extends AbstractEntity { + + @NotEmpty + private String name; + + @NotEmpty + private String shortName; + + @ManyToOne + @JoinColumn(name = "sport_id") + @NotNull + @JsonIgnoreProperties({ "positions" }) + private Sport sport; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "position") + @Nullable + private List roosters; +} \ No newline at end of file diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/FieldPositionRepository.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/FieldPositionRepository.java new file mode 100644 index 0000000..0f649cc --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/FieldPositionRepository.java @@ -0,0 +1,22 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface FieldPositionRepository extends JpaRepository { + + @Query("select p from FieldPosition p " + + "where lower(p.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + List findBySport(Sport sport); + + FieldPosition findByShortName(String searchTerm); + + List findByShortNameIgnoreCase(String shortName); + + FieldPosition findByShortNameAndSport(String shortName, Sport sport); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Player.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Player.java new file mode 100644 index 0000000..6517959 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Player.java @@ -0,0 +1,49 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = { + @Index(columnList = "firstName, lastName"), + @Index(columnList = "lastName, firstName") +}, uniqueConstraints = { + @UniqueConstraint(columnNames = { "firstName", "lastName" }) +}) +public class Player extends AbstractEntity { + + @NotNull + private String firstName; + + @NotNull + private String lastName; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "player") + @Nullable + private List roosters; + + public String getFullName() { + StringBuilder fullName = new StringBuilder(); + fullName.append(this.lastName); + fullName.append(", "); + fullName.append(this.firstName); + return fullName.toString(); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/PlayerRepository.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/PlayerRepository.java new file mode 100644 index 0000000..8431abd --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/PlayerRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface PlayerRepository extends JpaRepository { + + @Query("select p from Player p " + + "where lower(p.firstName) like lower(concat('%', :searchTerm, '%')) " + + "or lower(p.lastName) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); + + Player findByFirstNameAndLastName(String firstName, String lastName); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Rooster.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Rooster.java new file mode 100644 index 0000000..6410b07 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Rooster.java @@ -0,0 +1,51 @@ +package de.thpeetz.kontor.tysc.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "team_id, player_id, position_id")}, + uniqueConstraints = {@UniqueConstraint(name = "uniqueRooster", columnNames = {"year", "team_id", "player_id", "position_id"})} +) +public class Rooster extends AbstractEntity { + + private int year; + + @ManyToOne + @JoinColumn(name = "team_id") + @NotNull + private Team team; + + @ManyToOne + @JoinColumn(name = "player_id") + @NotNull + private Player player; + + @ManyToOne + @JoinColumn(name = "position_id") + @NotNull + private FieldPosition position; + + @Override + public String toString() { + return "Rooster{" + + "year=" + year + + ", team=" + team.getName() + + ", player=" + player.getFullName() + + ", position=" + position.getName() + + '}'; + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/RoosterRepository.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/RoosterRepository.java new file mode 100644 index 0000000..a24dda9 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/RoosterRepository.java @@ -0,0 +1,11 @@ +package de.thpeetz.kontor.tysc.data; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface RoosterRepository extends JpaRepository{ + + @Query("SELECT r FROM Rooster r WHERE r.player = ?1 AND r.team = ?2 AND r.position = ?3 AND r.year = ?4") + Rooster findByReferences(Player player, Team team, FieldPosition position, int year); + +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Sport.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Sport.java new file mode 100644 index 0000000..39333fd --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Sport.java @@ -0,0 +1,42 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.LinkedList; +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "name")}, + uniqueConstraints = {@UniqueConstraint(columnNames = { "name" })} +) +public class Sport extends AbstractEntity { + + @NotEmpty + @Column(unique = true) + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "sport") + @Nullable + private List teams = new LinkedList<>(); + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "sport") + @Nullable + private List positions = new LinkedList<>(); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/SportRepository.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/SportRepository.java new file mode 100644 index 0000000..35efe85 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/SportRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface SportRepository extends JpaRepository { + @Query("select s from Sport s " + + "where lower(s.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + Sport findByName(String name); + + List findByNameIgnoreCase(String name); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Team.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Team.java new file mode 100644 index 0000000..b61fdd2 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Team.java @@ -0,0 +1,50 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "name")}, + uniqueConstraints = {@UniqueConstraint(columnNames = { "name" })} +) +public class Team extends AbstractEntity { + + @NotEmpty + private String name; + + @NotEmpty + private String shortName; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "team") + @Nullable + private List roosters; + + @ManyToOne + @JoinColumn(name = "sport_id") + @NotNull + @JsonIgnoreProperties({ "teams" }) + private Sport sport; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/TeamRepository.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/TeamRepository.java new file mode 100644 index 0000000..9f518d9 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/TeamRepository.java @@ -0,0 +1,27 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +/** + * The repository interface for managing teams. + */ +public interface TeamRepository extends JpaRepository { + + /** + * Searches for teams based on a search term. + * + * @param searchTerm the search term to match against team names + * @return a list of teams matching the search term + */ + @Query("select t from Team t " + + "where lower(t.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + Team findByName(String name); + + List findByNameIgnoreCase(String name); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Vendor.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Vendor.java new file mode 100644 index 0000000..f405565 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Vendor.java @@ -0,0 +1,38 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Represents a vendor entity. + */ +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "name")}, + uniqueConstraints = {@UniqueConstraint(columnNames = "name")} +) +public class Vendor extends AbstractEntity { + + @NotEmpty + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "vendor") + @Nullable + private List cardSets; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/VendorRepository.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/VendorRepository.java new file mode 100644 index 0000000..4c7a304 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/VendorRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface VendorRepository extends JpaRepository { + + @Query("select v from Vendor v where lower(v.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + Vendor findByName(String name); + + List findByNameIgnoreCase(String name); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/services/CardService.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/services/CardService.java new file mode 100644 index 0000000..1cfedbd --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/services/CardService.java @@ -0,0 +1,93 @@ +package de.thpeetz.kontor.tysc.services; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import de.thpeetz.kontor.tysc.data.Card; +import de.thpeetz.kontor.tysc.data.CardRepository; +import de.thpeetz.kontor.tysc.data.CardSet; +import de.thpeetz.kontor.tysc.data.CardSetRepository; +import de.thpeetz.kontor.tysc.data.Vendor; +import de.thpeetz.kontor.tysc.data.VendorRepository; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class CardService { + + private VendorRepository vendorRepository; + + private CardSetRepository cardSetRepository; + + private CardRepository cardRepository; + + public CardService(VendorRepository vendorRepository, CardSetRepository cardSetRepository, CardRepository cardRepository) { + this.vendorRepository = vendorRepository; + this.cardSetRepository = cardSetRepository; + this.cardRepository = cardRepository; + } + + public List findAllVendors(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return vendorRepository.findAll(); + } else { + return vendorRepository.search(stringFilter); + } + } + + public void deleteVendor(Vendor vendor) { + vendorRepository.delete(vendor); + } + + public Vendor saveVendor(Vendor vendor) { + if (vendor == null) { + log.warn("Vendor is null. Are you sure you have connected your form to the application?"); + return null; + } + return vendorRepository.save(vendor); + } + + public List findAllCards(String stringFilter) { + log.info("find cards with filter: {}", stringFilter); + if (stringFilter == null || stringFilter.isEmpty()) { + List cards = cardRepository.findAll(); + log.debug("found {} cards", cards.size()); + return cards; + } else { + return cardRepository.search(stringFilter); + } + } + + public void deleteCard(Card card) { + cardRepository.delete(card); + } + + public Card saveCard(Card card) { + if (card == null) { + log.warn("Card is null. Are you sure you have connected your form to the application?"); + return null; + } + return cardRepository.save(card); + } + + public List findAllCardSets(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return cardSetRepository.findAll(); + } else { + return cardSetRepository.search(stringFilter); + } + } + + public CardSet saveCardSet(CardSet cardSet) { + if (cardSet == null) { + log.warn("CardSet is null. Are you sure you have connected your form to the application?"); + return null; + } + return cardSetRepository.save(cardSet); + } + + public void deleteCardSet(CardSet cardSet) { + cardSetRepository.delete(cardSet); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/services/SportService.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/services/SportService.java new file mode 100644 index 0000000..05482bf --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/services/SportService.java @@ -0,0 +1,147 @@ +package de.thpeetz.kontor.tysc.services; + +import java.util.List; + +import de.thpeetz.kontor.tysc.data.FieldPosition; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import de.thpeetz.kontor.tysc.data.Player; +import de.thpeetz.kontor.tysc.data.PlayerRepository; +import de.thpeetz.kontor.tysc.data.FieldPositionRepository; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.RoosterRepository; +import de.thpeetz.kontor.tysc.data.Sport; +import de.thpeetz.kontor.tysc.data.SportRepository; +import de.thpeetz.kontor.tysc.data.Team; +import de.thpeetz.kontor.tysc.data.TeamRepository; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class SportService { + + @Autowired + private SportRepository sportRepository; + + @Autowired + private TeamRepository teamRepository; + + @Autowired + private FieldPositionRepository fieldPositionRepository; + + @Autowired + private PlayerRepository playerRepository; + + @Autowired + private RoosterRepository roosterRepository; + + public List findAllSports(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return sportRepository.findAll(); + } else { + return sportRepository.search(stringFilter); + } + } + + public void deleteSport(Sport sport) { + sportRepository.delete(sport); + } + + public Sport saveSport(Sport sport) { + if (sport == null) { + log.warn("Sport is null. Are you sure you have connected your form to the application?"); + return null; + } + return sportRepository.save(sport); + } + + public List findAllPlayers(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return playerRepository.findAll(); + } else { + return playerRepository.search(stringFilter); + } + } + + public Player savePlayer(Player player) { + if (player == null) { + log.warn("Player is null. Are you sure you have connected yout form to the application?"); + return null; + } + return playerRepository.save(player); + } + + public void deletePlayer(Player player) { + playerRepository.delete(player); + } + + public List findAllTeams(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return teamRepository.findAll(); + } else { + return teamRepository.search(stringFilter); + } + } + + public void deleteTeam(Team team) { + teamRepository.delete(team); + } + + public void saveTeam(Team team) { + if (team == null) { + log.warn("Team is null. Are you sure you have connected your form to the application?"); + return; + } + teamRepository.save(team); + } + + public List findAllPositions(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return fieldPositionRepository.findAll(); + } else { + return fieldPositionRepository.search(stringFilter); + } + } + + public List findAllPositionsForSport(Sport sport) { + if (sport == null) { + return fieldPositionRepository.findAll(); + } else { + log.info("Find positions for Sport: {}", sport); + return fieldPositionRepository.findBySport(sport); + } + } + + public void savePosition(FieldPosition position) { + if (position == null) { + log.warn("Position is null. Are you sure you have connected your form to the application?"); + return; + } + fieldPositionRepository.save(position); + } + + public void deletePosition(FieldPosition position) { + fieldPositionRepository.delete(position); + } + + public List findAllRoosters() { + return roosterRepository.findAll(); + } + + public Rooster findRoosterByFields(Team team, Player player, FieldPosition position, Integer year) { + return roosterRepository.findByReferences(player, team, position, year); + } + + public void saveRooster(Rooster rooster) { + if (rooster == null) { + log.warn(""); + return; + } + roosterRepository.save(rooster); + } + + public void deleteRooster(Rooster rooster) { + roosterRepository.delete(rooster); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardForm.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardForm.java new file mode 100644 index 0000000..174afdc --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardForm.java @@ -0,0 +1,110 @@ +package de.thpeetz.kontor.tysc.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.Card; +import de.thpeetz.kontor.tysc.data.Rooster; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CardForm extends FormLayout { + + TextField cardNumber = new TextField("CardNumber"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Card.class); + + public CardForm() { + addClassName("card-form"); + binder.bindInstanceFields(this); + + add(cardNumber, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setCard(Card card) { + binder.setBean(card); + } + + public void setRoosters(List roosters) { + log.info("Setting roosters: {}", roosters); + } + + public abstract static class CardFormEvent extends ComponentEvent { + private Card card; + + protected CardFormEvent(CardForm source, Card card) { + super(source, false); + this.card = card; + } + + public Card getCard() { + return card; + } + } + + public static class SaveEvent extends CardFormEvent { + SaveEvent(CardForm source, Card card) { + super(source, card); + } + } + + public static class DeleteEvent extends CardFormEvent { + DeleteEvent(CardForm source, Card card) { + super(source, card); + } + } + + public static class CloseEvent extends CardFormEvent { + CloseEvent(CardForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardSetForm.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardSetForm.java new file mode 100644 index 0000000..d0e7e89 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardSetForm.java @@ -0,0 +1,103 @@ +package de.thpeetz.kontor.tysc.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.CardSet; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CardSetForm extends FormLayout { + + TextField name = new TextField("Name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(CardSet.class); + + public CardSetForm() { + addClassName("cardSet-form"); + binder.bindInstanceFields(this); + + add(name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setCardSet(CardSet cardSet) { + binder.setBean(cardSet); + } + + public abstract static class CardSetFormEvent extends ComponentEvent { + private CardSet cardSet; + + protected CardSetFormEvent(CardSetForm source, CardSet cardSet) { + super(source, false); + this.cardSet = cardSet; + } + + public CardSet getCardSet() { + return cardSet; + } + } + + public static class SaveEvent extends CardSetFormEvent { + SaveEvent(CardSetForm source, CardSet cardSet) { + super(source, cardSet); + } + } + + public static class DeleteEvent extends CardSetFormEvent { + DeleteEvent(CardSetForm source, CardSet cardSet) { + super(source, cardSet); + } + } + + public static class CloseEvent extends CardSetFormEvent { + CloseEvent(CardSetForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardSetView.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardSetView.java new file mode 100644 index 0000000..1a49896 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardSetView.java @@ -0,0 +1,129 @@ +package de.thpeetz.kontor.tysc.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.CardSet; +import de.thpeetz.kontor.tysc.services.CardService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.CARDSET_ROUTE, layout = MainLayout.class) +@PageTitle("CardSet | Tysc | Kontor") +public class CardSetView extends VerticalLayout { + + Grid grid = new Grid<>(CardSet.class); + TextField filterText = new TextField(); + CardSetForm form; + CardService service; + + public CardSetView(CardService service) { + this.service = service; + addClassName("cardSet-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("cardSet-grid"); + grid.setSizeFull(); + grid.setColumns("name", "vendor.name", "parallelSet", "insertSet"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editCardSet(event.getValue())); + } + + public CardSetForm getForm() { + return form; + } + + private void configureForm() { + form = new CardSetForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveCardSet); + form.addDeleteListener(this::deleteCardSet); + form.addCloseListener(e -> closeEditor()); + } + + private void saveCardSet(CardSetForm.SaveEvent event) { + service.saveCardSet(event.getCardSet()); + updateList(); + closeEditor(); + } + + private void deleteCardSet(CardSetForm.DeleteEvent event) { + service.deleteCardSet(event.getCardSet()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addCardSetButton = new Button("Add cardSet"); + addCardSetButton.addClickListener(click -> addCardSet()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addCardSetButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editCardSet(CardSet cardSet) { + if (cardSet == null) { + closeEditor(); + } else { + form.setCardSet(cardSet); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setCardSet(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addCardSet() { + grid.asSingleSelect().clear(); + editCardSet(new CardSet()); + } + + public void updateList() { + grid.setItems(service.findAllCardSets(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardView.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardView.java new file mode 100644 index 0000000..89cc1ce --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/CardView.java @@ -0,0 +1,129 @@ +package de.thpeetz.kontor.tysc.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.Card; +import de.thpeetz.kontor.tysc.services.CardService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.CARD_ROUTE, layout = MainLayout.class) +@PageTitle("Card | Tysc | Kontor") +public class CardView extends VerticalLayout { + + Grid grid = new Grid<>(Card.class); + TextField filterText = new TextField(); + CardForm form; + CardService service; + + public CardView(CardService service) { + this.service = service; + addClassName("card-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("card-grid"); + grid.setSizeFull(); + grid.setColumns("cardNumber", "year", "vendor.name", "cardSet.name", "rooster.player.fullName"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editCard(event.getValue())); + } + + public CardForm getForm() { + return form; + } + + private void configureForm() { + form = new CardForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveCard); + form.addDeleteListener(this::deleteCard); + form.addCloseListener(e -> closeEditor()); + } + + private void saveCard(CardForm.SaveEvent event) { + service.saveCard(event.getCard()); + updateList(); + closeEditor(); + } + + private void deleteCard(CardForm.DeleteEvent event) { + service.deleteCard(event.getCard()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addCardButton = new Button("Add card"); + addCardButton.addClickListener(click -> addCard()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addCardButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editCard(Card card) { + if (card == null) { + closeEditor(); + } else { + form.setCard(card); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setCard(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addCard() { + grid.asSingleSelect().clear(); + editCard(new Card()); + } + + public void updateList() { + grid.setItems(service.findAllCards(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PlayerForm.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PlayerForm.java new file mode 100644 index 0000000..9730103 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PlayerForm.java @@ -0,0 +1,116 @@ +package de.thpeetz.kontor.tysc.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.Player; +import de.thpeetz.kontor.tysc.data.Rooster; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PlayerForm extends FormLayout { + + TextField firstName = new TextField("First Name"); + TextField lastName = new TextField("Last Name"); + Grid roosters = new Grid<>(Rooster.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Player.class); + + public PlayerForm() { + addClassName("player-form"); + binder.bindInstanceFields(this); + + roosters.setColumns("team.name", "position.name", "year"); + roosters.getColumns().forEach(col -> col.setAutoWidth(true)); + add(firstName, lastName, roosters, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setPlayer(Player player) { + binder.setBean(player); + } + + public void setRoosters(List roosters) { + log.info("Setting roosters: {}", roosters); + this.roosters.setItems(roosters); + } + + public abstract static class PlayerFormEvent extends ComponentEvent { + private Player player; + + protected PlayerFormEvent(PlayerForm source, Player player) { + super(source, false); + this.player = player; + } + + public Player getPlayer() { + return player; + } + } + + public static class SaveEvent extends PlayerFormEvent { + SaveEvent(PlayerForm source, Player player) { + super(source, player); + } + } + + public static class DeleteEvent extends PlayerFormEvent { + DeleteEvent(PlayerForm source, Player player) { + super(source, player); + } + } + + public static class CloseEvent extends PlayerFormEvent { + CloseEvent(PlayerForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PlayerView.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PlayerView.java new file mode 100644 index 0000000..56d9588 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PlayerView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.tysc.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.Player; +import de.thpeetz.kontor.tysc.services.SportService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.PLAYER_ROUTE, layout = MainLayout.class) +@PageTitle("Player | Tysc | Kontor") +public class PlayerView extends VerticalLayout { + + Grid grid = new Grid<>(Player.class); + TextField filterText = new TextField(); + PlayerForm form; + SportService service; + + public PlayerView(SportService service) { + this.service = service; + addClassName("player-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("player-grid"); + grid.setSizeFull(); + grid.setColumns("firstName", "lastName"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editPlayer(event.getValue())); + } + + public PlayerForm getForm() { + return form; + } + + private void configureForm() { + form = new PlayerForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::savePlayer); + form.addDeleteListener(this::deletePlayer); + form.addCloseListener(e -> closeEditor()); + } + + private void savePlayer(PlayerForm.SaveEvent event) { + service.savePlayer(event.getPlayer()); + updateList(); + closeEditor(); + } + + private void deletePlayer(PlayerForm.DeleteEvent event) { + service.deletePlayer(event.getPlayer()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addPlayerButton = new Button("Add player"); + addPlayerButton.addClickListener(click -> addPlayer()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addPlayerButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editPlayer(Player player) { + if (player == null) { + closeEditor(); + } else { + form.setPlayer(player); + form.setRoosters(player.getRoosters()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setPlayer(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addPlayer() { + grid.asSingleSelect().clear(); + editPlayer(new Player()); + } + + public void updateList() { + grid.setItems(service.findAllPlayers(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PositionForm.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PositionForm.java new file mode 100644 index 0000000..f5b49bb --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PositionForm.java @@ -0,0 +1,115 @@ +package de.thpeetz.kontor.tysc.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.FieldPosition; +import de.thpeetz.kontor.tysc.data.Rooster; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PositionForm extends FormLayout { + + TextField name = new TextField("Name"); + Grid roosters = new Grid<>(Rooster.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(FieldPosition.class); + + public PositionForm() { + addClassName("position-form"); + binder.bindInstanceFields(this); + + roosters.setColumns("player.fullName", "team.name", "year"); + roosters.getColumns().forEach(col -> col.setAutoWidth(true)); + add(name, roosters, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setPosition(FieldPosition position) { + binder.setBean(position); + } + + public void setRoosters(List roosters) { + log.info("Setting roosters: {}", roosters); + this.roosters.setItems(roosters); + } + + public abstract static class PositionFormEvent extends ComponentEvent { + private FieldPosition position; + + protected PositionFormEvent(PositionForm source, FieldPosition position) { + super(source, false); + this.position = position; + } + + public FieldPosition getPosition() { + return position; + } + } + + public static class SaveEvent extends PositionFormEvent { + SaveEvent(PositionForm source, FieldPosition position) { + super(source, position); + } + } + + public static class DeleteEvent extends PositionFormEvent { + DeleteEvent(PositionForm source, FieldPosition position) { + super(source, position); + } + } + + public static class CloseEvent extends PositionFormEvent { + CloseEvent(PositionForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PositionView.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PositionView.java new file mode 100644 index 0000000..ad71bf0 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/PositionView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.tysc.views; + +import de.thpeetz.kontor.tysc.data.FieldPosition; +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.services.SportService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.POSITION_ROUTE, layout = MainLayout.class) +@PageTitle("Position | Tysc | Kontor") +public class PositionView extends VerticalLayout { + + Grid grid = new Grid<>(FieldPosition.class); + TextField filterText = new TextField(); + PositionForm form; + SportService service; + + public PositionView(SportService service) { + this.service = service; + addClassName("position-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("position-grid"); + grid.setSizeFull(); + grid.setColumns("shortName", "name", "sport.name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editPosition(event.getValue())); + } + + public PositionForm getForm() { + return form; + } + + private void configureForm() { + form = new PositionForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::savePosition); + form.addDeleteListener(this::deletePosition); + form.addCloseListener(e -> closeEditor()); + } + + private void savePosition(PositionForm.SaveEvent event) { + service.savePosition(event.getPosition()); + updateList(); + closeEditor(); + } + + private void deletePosition(PositionForm.DeleteEvent event) { + service.deletePosition(event.getPosition()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addPositionButton = new Button("Add position"); + addPositionButton.addClickListener(click -> addPosition()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addPositionButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editPosition(FieldPosition fieldPosition) { + if (fieldPosition == null) { + closeEditor(); + } else { + form.setPosition(fieldPosition); + form.setRoosters(fieldPosition.getRoosters()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setPosition(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addPosition() { + grid.asSingleSelect().clear(); + editPosition(new FieldPosition()); + } + + public void updateList() { + grid.setItems(service.findAllPositions(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/RoosterForm.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/RoosterForm.java new file mode 100644 index 0000000..5f9d259 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/RoosterForm.java @@ -0,0 +1,117 @@ +package de.thpeetz.kontor.tysc.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.IntegerField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.FieldPosition; +import de.thpeetz.kontor.tysc.data.Player; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.Team; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class RoosterForm extends FormLayout { + IntegerField year = new IntegerField("Year"); + ComboBox team = new ComboBox<>("Team"); + ComboBox player = new ComboBox<>("Player"); + ComboBox position = new ComboBox<>("Position"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Rooster.class); + + public RoosterForm(List teams, List players, List positions) { + addClassName("rooster-form"); + binder.bindInstanceFields(this); + + team.setItems(teams); + team.setItemLabelGenerator(Team::getName); + player.setItems(players); + player.setItemLabelGenerator(Player::getFullName); + position.setItems(positions); + position.setItemLabelGenerator(FieldPosition::getName); + add(year, team, player, position, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setRooster(Rooster rooster) { + binder.setBean(rooster); + } + + public abstract static class RoosterFormEvent extends ComponentEvent { + private Rooster rooster; + + protected RoosterFormEvent(RoosterForm source, Rooster rooster) { + super(source, false); + this.rooster = rooster; + } + + public Rooster getRooster() { + return rooster; + } + } + + public static class SaveEvent extends RoosterFormEvent { + SaveEvent(RoosterForm source, Rooster rooster) { + super(source, rooster); + } + } + + public static class DeleteEvent extends RoosterFormEvent { + DeleteEvent(RoosterForm source, Rooster rooster) { + super(source, rooster); + } + } + + public static class CloseEvent extends RoosterFormEvent { + CloseEvent(RoosterForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/RoosterView.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/RoosterView.java new file mode 100644 index 0000000..92da6eb --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/RoosterView.java @@ -0,0 +1,121 @@ +package de.thpeetz.kontor.tysc.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.services.SportService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.ROOSTER_ROUTE, layout = MainLayout.class) +@PageTitle("Rooster | Tysc | Kontor") +public class RoosterView extends VerticalLayout { + + Grid grid = new Grid<>(Rooster.class); + RoosterForm form; + SportService service; + + public RoosterView(SportService service) { + this.service = service; + addClassName("rooster-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("rooster-grid"); + grid.setSizeFull(); + grid.setColumns("year", "team.name", "player.fullName", "position.name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editRooster(event.getValue())); + } + + public RoosterForm getForm() { + return form; + } + + private void configureForm() { + form = new RoosterForm(service.findAllTeams(null), service.findAllPlayers(null), service.findAllPositions(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveRooster); + form.addDeleteListener(this::deleteRooster); + form.addCloseListener(e -> closeEditor()); + } + + private void saveRooster(RoosterForm.SaveEvent event) { + service.saveRooster(event.getRooster()); + updateList(); + closeEditor(); + } + + private void deleteRooster(RoosterForm.DeleteEvent event) { + service.deleteRooster(event.getRooster()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + Button addRoosterButton = new Button("Add Rooster"); + addRoosterButton.addClickListener(click -> addRooster()); + + HorizontalLayout toolbar = new HorizontalLayout(addRoosterButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editRooster(Rooster rooster) { + if (rooster == null) { + closeEditor(); + } else { + form.setRooster(rooster); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setRooster(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addRooster() { + grid.asSingleSelect().clear(); + editRooster(new Rooster()); + } + + public void updateList() { + grid.setItems(service.findAllRoosters()); + } +} \ No newline at end of file diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/SportForm.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/SportForm.java new file mode 100644 index 0000000..30582e1 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/SportForm.java @@ -0,0 +1,103 @@ +package de.thpeetz.kontor.tysc.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.Sport; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SportForm extends FormLayout { + + TextField name = new TextField("Name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Sport.class); + + public SportForm() { + addClassName("sport-form"); + binder.bindInstanceFields(this); + + add(name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setSport(Sport sport) { + binder.setBean(sport); + } + + public abstract static class SportFormEvent extends ComponentEvent { + private Sport sport; + + protected SportFormEvent(SportForm source, Sport sport) { + super(source, false); + this.sport = sport; + } + + public Sport getSport() { + return sport; + } + } + + public static class SaveEvent extends SportFormEvent { + SaveEvent(SportForm source, Sport sport) { + super(source, sport); + } + } + + public static class DeleteEvent extends SportFormEvent { + DeleteEvent(SportForm source, Sport sport) { + super(source, sport); + } + } + + public static class CloseEvent extends SportFormEvent { + CloseEvent(SportForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/SportView.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/SportView.java new file mode 100644 index 0000000..c6008c3 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/SportView.java @@ -0,0 +1,129 @@ +package de.thpeetz.kontor.tysc.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.Sport; +import de.thpeetz.kontor.tysc.services.SportService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.SPORT_ROUTE, layout = MainLayout.class) +@PageTitle("Sport | Tysc | Kontor") +public class SportView extends VerticalLayout { + + Grid grid = new Grid<>(Sport.class); + TextField filterText = new TextField(); + SportForm form; + SportService service; + + public SportView(SportService service) { + this.service = service; + addClassName("sport-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("sport-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editSport(event.getValue())); + } + + public SportForm getForm() { + return form; + } + + private void configureForm() { + form = new SportForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveSport); + form.addDeleteListener(this::deleteSport); + form.addCloseListener(e -> closeEditor()); + } + + private void saveSport(SportForm.SaveEvent event) { + service.saveSport(event.getSport()); + updateList(); + closeEditor(); + } + + private void deleteSport(SportForm.DeleteEvent event) { + service.deleteSport(event.getSport()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addSportButton = new Button("Add sport"); + addSportButton.addClickListener(click -> addSport()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addSportButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editSport(Sport sport) { + if (sport == null) { + closeEditor(); + } else { + form.setSport(sport); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setSport(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addSport() { + grid.asSingleSelect().clear(); + editSport(new Sport()); + } + + public void updateList() { + grid.setItems(service.findAllSports(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/TeamForm.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/TeamForm.java new file mode 100644 index 0000000..63cb56b --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/TeamForm.java @@ -0,0 +1,115 @@ +package de.thpeetz.kontor.tysc.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.Team; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TeamForm extends FormLayout { + + TextField name = new TextField("Name"); + Grid roosters = new Grid<>(Rooster.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Team.class); + + public TeamForm() { + addClassName("team-form"); + binder.bindInstanceFields(this); + + roosters.setColumns("player.fullName", "position.name", "year"); + roosters.getColumns().forEach(col -> col.setAutoWidth(true)); + add(name, roosters, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setTeam(Team team) { + binder.setBean(team); + } + + public void setRoosters(List roosters) { + log.info("Setting roosters: {}", roosters); + this.roosters.setItems(roosters); + } + + public abstract static class TeamFormEvent extends ComponentEvent { + private Team team; + + protected TeamFormEvent(TeamForm source, Team team) { + super(source, false); + this.team = team; + } + + public Team getTeam() { + return team; + } + } + + public static class SaveEvent extends TeamFormEvent { + SaveEvent(TeamForm source, Team team) { + super(source, team); + } + } + + public static class DeleteEvent extends TeamFormEvent { + DeleteEvent(TeamForm source, Team team) { + super(source, team); + } + } + + public static class CloseEvent extends TeamFormEvent { + CloseEvent(TeamForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/TeamView.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/TeamView.java new file mode 100644 index 0000000..1e48f98 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/TeamView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.tysc.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.Team; +import de.thpeetz.kontor.tysc.services.SportService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.TEAM_ROUTE, layout = MainLayout.class) +@PageTitle("Team | Tysc | Kontor") +public class TeamView extends VerticalLayout { + + Grid grid = new Grid<>(Team.class); + TextField filterText = new TextField(); + TeamForm form; + SportService service; + + public TeamView(SportService service) { + this.service = service; + addClassName("team-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("team-grid"); + grid.setSizeFull(); + grid.setColumns("name", "shortName", "sport.name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editTeam(event.getValue())); + } + + public TeamForm getForm() { + return form; + } + + private void configureForm() { + form = new TeamForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveTeam); + form.addDeleteListener(this::deleteTeam); + form.addCloseListener(e -> closeEditor()); + } + + private void saveTeam(TeamForm.SaveEvent event) { + service.saveTeam(event.getTeam()); + updateList(); + closeEditor(); + } + + private void deleteTeam(TeamForm.DeleteEvent event) { + service.deleteTeam(event.getTeam()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addTeamButton = new Button("Add team"); + addTeamButton.addClickListener(click -> addTeam()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addTeamButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editTeam(Team team) { + if (team == null) { + closeEditor(); + } else { + form.setTeam(team); + form.setRoosters(team.getRoosters()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setTeam(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addTeam() { + grid.asSingleSelect().clear(); + editTeam(new Team()); + } + + public void updateList() { + grid.setItems(service.findAllTeams(filterText.getValue())); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/TyscLayout.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/TyscLayout.java new file mode 100644 index 0000000..66fb09b --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/TyscLayout.java @@ -0,0 +1,41 @@ +package de.thpeetz.kontor.tysc.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.common.views.KontorLayoutUtil; +import de.thpeetz.kontor.security.SecurityService; +import de.thpeetz.kontor.tysc.TyscConstants; +import lombok.extern.slf4j.Slf4j; + +/** + * Represents a custom layout for the comic view in the application. + * This layout extends the AppLayout class. + */ +@Slf4j +public class TyscLayout extends AppLayout { + + private final AdminService adminService; + + private final SecurityService securityService; + + public TyscLayout(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + + KontorLayoutUtil layout = new KontorLayoutUtil(this, adminService, securityService); + layout.setSecondaryNavigation(getSecondaryNavigation()); + layout.createHeader(TyscConstants.TYSC); + } + + private HorizontalLayout getSecondaryNavigation() { + HorizontalLayout navigation = new HorizontalLayout(); + navigation.addClassNames(LumoUtility.JustifyContent.CENTER, LumoUtility.Gap.SMALL, LumoUtility.Height.MEDIUM); + navigation.add(TyscConstants.getSportLink(), TyscConstants.getTeamLink(), TyscConstants.getPositionLink(), + TyscConstants.getPlayerLink(), TyscConstants.getRoosterLink(), TyscConstants.getVendorLink(), + TyscConstants.getCardSetLink(), TyscConstants.getCardLink()); + return navigation; + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/VendorForm.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/VendorForm.java new file mode 100644 index 0000000..254b1ff --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/VendorForm.java @@ -0,0 +1,115 @@ +package de.thpeetz.kontor.tysc.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.Vendor; +import de.thpeetz.kontor.tysc.data.CardSet; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class VendorForm extends FormLayout { + + TextField name = new TextField("Name"); + Grid cardSets = new Grid<>(CardSet.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Vendor.class); + + public VendorForm() { + addClassName("vendor-form"); + binder.bindInstanceFields(this); + + cardSets.setColumns("name", "parallelSet"); + cardSets.getColumns().forEach(col -> col.setAutoWidth(true)); + add(name, cardSets, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setVendor(Vendor vendor) { + binder.setBean(vendor); + } + + public void setCardSets(List sets) { + log.info("Setting card sets: {}", sets); + this.cardSets.setItems(sets); + } + + public abstract static class VendorFormEvent extends ComponentEvent { + private Vendor vendor; + + protected VendorFormEvent(VendorForm source, Vendor vendor) { + super(source, false); + this.vendor = vendor; + } + + public Vendor getVendor() { + return vendor; + } + } + + public static class SaveEvent extends VendorFormEvent { + SaveEvent(VendorForm source, Vendor vendor) { + super(source, vendor); + } + } + + public static class DeleteEvent extends VendorFormEvent { + DeleteEvent(VendorForm source, Vendor vendor) { + super(source, vendor); + } + } + + public static class CloseEvent extends VendorFormEvent { + CloseEvent(VendorForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/views/VendorView.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/VendorView.java new file mode 100644 index 0000000..08b5d32 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/views/VendorView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.tysc.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.Vendor; +import de.thpeetz.kontor.tysc.services.CardService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.VENDOR_ROUTE, layout = MainLayout.class) +@PageTitle("Vendor | Tysc | Kontor") +public class VendorView extends VerticalLayout { + + Grid grid = new Grid<>(Vendor.class); + TextField filterText = new TextField(); + VendorForm form; + CardService service; + + public VendorView(CardService service) { + this.service = service; + addClassName("vendor-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("vendor-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editVendor(event.getValue())); + } + + public VendorForm getForm() { + return form; + } + + private void configureForm() { + form = new VendorForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveVendor); + form.addDeleteListener(this::deleteVendor); + form.addCloseListener(e -> closeEditor()); + } + + private void saveVendor(VendorForm.SaveEvent event) { + service.saveVendor(event.getVendor()); + updateList(); + closeEditor(); + } + + private void deleteVendor(VendorForm.DeleteEvent event) { + service.deleteVendor(event.getVendor()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addVendorButton = new Button("Add vendor"); + addVendorButton.addClickListener(click -> addVendor()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addVendorButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editVendor(Vendor vendor) { + if (vendor == null) { + closeEditor(); + } else { + form.setVendor(vendor); + form.setCardSets(vendor.getCardSets()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setVendor(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addVendor() { + grid.asSingleSelect().clear(); + editVendor(new Vendor()); + } + + public void updateList() { + grid.setItems(service.findAllVendors(filterText.getValue())); + } +} diff --git a/springboot/src/main/resources/META-INF/resources/icons/icon.png b/springboot/src/main/resources/META-INF/resources/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..77bb2c9ad73c16d2878c34fef9443c91466178df GIT binary patch literal 45967 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 literal 0 HcmV?d00001 diff --git a/springboot/src/main/resources/META-INF/resources/images/offline.png b/springboot/src/main/resources/META-INF/resources/images/offline.png new file mode 100644 index 0000000000000000000000000000000000000000..723e24ce7525be86a728d07a8e98f6e1753b745b GIT binary patch literal 9507 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 literal 0 HcmV?d00001 diff --git a/springboot/src/main/resources/META-INF/resources/offline.html b/springboot/src/main/resources/META-INF/resources/offline.html new file mode 100644 index 0000000..791d1e8 --- /dev/null +++ b/springboot/src/main/resources/META-INF/resources/offline.html @@ -0,0 +1,38 @@ + + + + + + + Offline | Vaadin CRM + + + + +
+ VaadinCRM is offline +

Oh deer, you're offline

+

Your internet connection is offline. Get back online to continue using Vaadin CRM.

+
+ + + diff --git a/springboot/src/main/resources/application.yml b/springboot/src/main/resources/application.yml new file mode 100644 index 0000000..fd8dfff --- /dev/null +++ b/springboot/src/main/resources/application.yml @@ -0,0 +1,67 @@ +server: + port: 8085 +app: + name: 'Kontor' + shortName: 'Kontor' + description: 'Kontor is a Spring Boot application' +spring: + profiles: + active: local,dev,test,prod + devtools: + add-properties: false + datasource: + driverClassName: org.mariadb.jdbc.Driver + url: jdbc:mariadb://localhost:3306/kontor + username: 'kontor' + password: 'kontor' + #driverClassName: org.hsqldb.jdbc.JDBCDriver + #url: jdbc:hsqldb:file:kontorHSQLDB + #username: 'sa' + #password: 'sa' + #jpa + #database-platform: org.hibernate.community.dialect.SQLiteDialect + #datasource + #driverClassName: org.sqlite.JDBC + #url: "jdbc:sqlite:file:./kontorDb?cache=shared" + #username=sa + #password=sa + jpa: + defer-datasource-initialization: true + #hibernate.ddl-auto=create-drop + hibernate: + ddl-auto: update + #ddl-auto: create-drop + show-sql: false + sql: + init: + mode: never + mustache: + check-template-location: false +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: always +logging: + level: + org: + atmosphere: INFO + hibernate: INFO + springframework: + web: INFO + guru: + springframework: + controllers: DEBUG +jwt: + auth: + secret: 'J6GOtcwC2NJI1l0VkHu20PacPFGTxpirBxWwynoHjsc=' +mail: + protocol: 'imap' + host: 'corky.svpdata.eu' + port: 143 + userName: 'thomas.peetz@thpeetz.de' + password: 'fS9f4JYDIO7A' + starttls: true diff --git a/springboot/src/main/resources/banner.txt b/springboot/src/main/resources/banner.txt new file mode 100644 index 0000000..b21c682 --- /dev/null +++ b/springboot/src/main/resources/banner.txt @@ -0,0 +1,8 @@ +,--. ,--. ,--. +| .' / ,---. ,--,--, ,-' '-. ,---. ,--.--. +| . ' | .-. || \'-. .-'| .-. || .--' +| |\ \' '-' '| || | | | ' '-' '| | +`--' '--' `---' `--''--' `--' `---' `--' + +${application.title} ${application.version} +Powered by Spring Boot ${spring-boot.version} \ No newline at end of file diff --git a/springboot/src/main/resources/logback-spring.xml b/springboot/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..b085f8c --- /dev/null +++ b/springboot/src/main/resources/logback-spring.xml @@ -0,0 +1,42 @@ + + + + + + + + + %white(%d{ISO8601}) %highlight(%-5level) [%green(%t)] %yellow(%C{1}) : %msg%n%throwable + + + + + + ${LOGS}/spring-boot-logger.log + + %d %p %C{1} [%t] %m%n + + + + + ${LOGS}/archived/spring-boot-logger-%d{yyyy-MM-dd}.%i.log + + + 10MB + + + + + + + + + + + + diff --git a/springboot/src/test/java/de/thpeetz/kontor/ApplicationTests.java b/springboot/src/test/java/de/thpeetz/kontor/ApplicationTests.java new file mode 100644 index 0000000..abf5c95 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/ApplicationTests.java @@ -0,0 +1,13 @@ +package de.thpeetz.kontor; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ApplicationTests { + + @Test + void contextLoads() { + + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/TestConstants.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/TestConstants.java new file mode 100644 index 0000000..bb9d04d --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/TestConstants.java @@ -0,0 +1,19 @@ +package de.thpeetz.kontor.bookshelf; + +public class TestConstants { + + public static final Integer ARTICLEAUTHOR_COUNT = 0; + public static final Integer ARTICLE_COUNT = 0; + public static final Integer AUTHOR_COUNT = 1; + public static final Integer BOOKAUTHOR_COUNT = 0; + public static final Integer BOOK_COUNT = 0; + public static final Integer PUBLISHER_COUNT = 0; + public static final String ARTICLE_TITLE = "Title"; + public static final String AUTHOR_FIRSTNAME = "Firstname"; + public static final String AUTHOR_LASTNAME = "Lastname"; + public static final String PUBLISHER_NIKOL = "Nikol Verlags GmbH"; + public static final String PUBLISHER_NAME = "Publisher"; + public static final String PUBLISHER_SEARCH = "kol"; + public static final String BOOK_TITLE = "Book Title"; + public static final String BOOK_ISBN = "978-3-123-467-890"; +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepositoryTest.java new file mode 100644 index 0000000..b54b976 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepositoryTest.java @@ -0,0 +1,74 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@Disabled +class ArticleAuthorRepositoryTest { + + @Autowired + ArticleAuthorRepository articleAuthorRepository; + + @Autowired + AuthorRepository authorRepository; + + @Autowired + ArticleRepository articleRepository; + + @Test + @Order(1) + void saveArticleAuthor() { + Author author = new Author(); + author.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + author.setLastName(TestConstants.AUTHOR_LASTNAME); + Author savedAuthor = authorRepository.save(author); + assertEquals(TestConstants.AUTHOR_COUNT+1, authorRepository.findAll().size()); + Article article = new Article(); + article.setTitle(TestConstants.ARTICLE_TITLE); + Article savedArticle = articleRepository.save(article); + ArticleAuthor articleAuthor = new ArticleAuthor(); + articleAuthor.setArticle(article); + articleAuthor.setAuthor(author); + ArticleAuthor savedArticleAuthor = articleAuthorRepository.save(articleAuthor); + assertEquals(TestConstants.ARTICLEAUTHOR_COUNT+1, articleAuthorRepository.findAll().size()); + } + + @Test + @Order(2) + void testFindByArticle() { + Article article = articleRepository.findByTitle(TestConstants.ARTICLE_TITLE).get(0); + assertEquals(1, articleAuthorRepository.findByArticle(article).size()); + } + + @Test + @Order(3) + void testFindByAuthor() { + Author author = authorRepository.findByFirstNameAndLastName(TestConstants.AUTHOR_FIRSTNAME, TestConstants.AUTHOR_LASTNAME); + assertEquals(1, articleAuthorRepository.findByAuthor(author).size()); + } + + @Test + @Order(4) + void deleteArticleAuthor() { + assertEquals(1, articleAuthorRepository.findAll().size()); + ArticleAuthor articleAuthor = articleAuthorRepository.findAll().get(0); + Author author = articleAuthor.getAuthor(); + author.getArticleAuthors().remove(articleAuthor); + author = authorRepository.save(author); + Article article = articleAuthor.getArticle(); + article.getAuthors().remove(articleAuthor); + article = articleRepository.save(article); + articleAuthorRepository.delete(articleAuthor); + assertEquals(TestConstants.ARTICLEAUTHOR_COUNT, articleAuthorRepository.findAll().size()); + authorRepository.delete(author); + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + articleRepository.delete(article); + assertEquals(TestConstants.ARTICLE_COUNT, articleRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorTest.java new file mode 100644 index 0000000..bd14fd3 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorTest.java @@ -0,0 +1,77 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +@Disabled +class ArticleAuthorTest { + + @Autowired + ArticleRepository articleRepository; + + @Autowired + AuthorRepository authorRepository; + + @Autowired + ArticleAuthorRepository articleAuthorRepository; + + @Test + @Order(1) + void checkInitialLoad() { + assertEquals(TestConstants.ARTICLEAUTHOR_COUNT, articleAuthorRepository.findAll().size()); + } + + @Test + @Order(2) + void exceptionThrownWhenSavingEmptyValues() { + ArticleAuthor articleAuthor = new ArticleAuthor(); + assertThrows(TransactionSystemException.class, () -> { + articleAuthorRepository.save(articleAuthor); + }); + } + + @Test + @Order(3) + void exceptionThrownWhenSavingIdenticalEntry() { + Author author = new Author(); + author.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + author.setLastName(TestConstants.AUTHOR_LASTNAME); + Author savedAuthor = authorRepository.save(author); + Article article = new Article(); + article.setTitle(TestConstants.ARTICLE_TITLE); + Article savedArticle = articleRepository.save(article); + ArticleAuthor articleAuthor = new ArticleAuthor(); + articleAuthor.setArticle(article); + articleAuthor.setAuthor(author); + ArticleAuthor savedArticleAuthor = articleAuthorRepository.save(articleAuthor); + ArticleAuthor articleAuthor1 = new ArticleAuthor(); + articleAuthor1.setArticle(article); + articleAuthor1.setAuthor(author); + assertThrows(DataIntegrityViolationException.class, () -> { + articleAuthorRepository.save(articleAuthor1); + }); + articleAuthorRepository.delete(savedArticleAuthor); + assertEquals(TestConstants.ARTICLEAUTHOR_COUNT, articleAuthorRepository.findAll().size()); + savedAuthor.getArticleAuthors().remove(savedArticleAuthor); + authorRepository.save(savedAuthor); + authorRepository.delete(savedAuthor); + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + savedArticle.getAuthors().remove(savedArticleAuthor); + articleRepository.save(savedArticle); + articleRepository.delete(savedArticle); + assertEquals(TestConstants.ARTICLE_COUNT, articleRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleRepositoryTest.java new file mode 100644 index 0000000..7d74119 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleRepositoryTest.java @@ -0,0 +1,61 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class ArticleRepositoryTest { + + @Autowired + ArticleRepository articleRepository; + + @Test + @Order(1) + void checkInitialLoad() { + assertEquals(TestConstants.ARTICLE_COUNT, articleRepository.findAll().size()); + } + + @Test + @Order(2) + void saveArticle() { + Article article = new Article(); + article.setTitle(TestConstants.ARTICLE_TITLE); + articleRepository.save(article); + assertEquals(TestConstants.ARTICLE_COUNT+1, articleRepository.findAll().size()); + } + + @Test + @Order(3) + void search() { + List
articles = articleRepository.search(TestConstants.ARTICLE_TITLE.substring(2,5)); + assertEquals(1, articles.size()); + assertEquals(0, articleRepository.search(TestConstants.BOOK_TITLE).size()); + } + + @Test + @Order(4) + void findByTitle() { + List
articles = articleRepository.search(TestConstants.ARTICLE_TITLE); + assertEquals(1, articles.size()); + } + + @Test + @Order(5) + void deleteArticle() { + List
articles = articleRepository.findAll(); + assertEquals(1, articles.size()); + assertEquals(0, articles.get(0).getAuthors().size()); + articleRepository.delete(articles.get(0)); + assertEquals(TestConstants.ARTICLE_COUNT, articleRepository.findAll().size()); + } +} \ No newline at end of file diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleTest.java new file mode 100644 index 0000000..2179dfe --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleTest.java @@ -0,0 +1,45 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +class ArticleTest { + + @Autowired + ArticleRepository articleRepository; + + @Test + void checkInitialLoad() { + assertEquals(TestConstants.BOOK_COUNT, articleRepository.findAll().size()); + } + + @Test + void exceptionThrownWhenSavingEmptyFields() { + Article article = new Article(); + assertThrows(TransactionSystemException.class, () -> { + articleRepository.save(article); + }); + } + + @Test + void exceptionThrownWhenSavingIdenticalTitle() { + Article article = new Article(); + article.setTitle(TestConstants.ARTICLE_TITLE); + Article savedArticle = articleRepository.save(article); + Article article1 = new Article(); + article1.setTitle(article.getTitle()); + assertThrows(DataIntegrityViolationException.class, ()-> { + articleRepository.save(article1); + }); + articleRepository.delete(savedArticle); + assertEquals(TestConstants.ARTICLE_COUNT, articleRepository.findAll().size()); + } +} \ No newline at end of file diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorRepositoryTest.java new file mode 100644 index 0000000..f984d11 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorRepositoryTest.java @@ -0,0 +1,69 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +@Disabled +class AuthorRepositoryTest { + + @Autowired + AuthorRepository authorRepository; + + @Test + @Order(1) + void checkInitialLoad() { + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + } + + @Test + @Order(2) + void saveAutor() { + Author author = new Author(); + author.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + author.setLastName(TestConstants.AUTHOR_LASTNAME); + authorRepository.save(author); + assertEquals(TestConstants.AUTHOR_COUNT+1, authorRepository.findAll().size()); + } + + @Test + @Order(3) + void findAuthor() { + Author existingAuthor = authorRepository.findAll().get(0); + assertNotNull(existingAuthor); + Author found = authorRepository.findByFirstNameAndLastName(existingAuthor.getFirstName(), existingAuthor.getLastName()); + assertEquals(existingAuthor, found); + } + + @Test + @Order(4) + void searchAuthor() { + List authors = authorRepository.search("glas"); + assertEquals(1, authors.size()); + assertEquals("Douglas", authors.get(0).getFirstName()); + List authors2 = authorRepository.search("dams"); + assertEquals(1, authors2.size()); + assertEquals("Adams", authors2.get(0).getLastName()); + } + + @Test + @Order(5) + void deleteAuthor() { + Author existingAuthor = authorRepository.findByFirstNameAndLastName(TestConstants.AUTHOR_FIRSTNAME, TestConstants.AUTHOR_LASTNAME); + assertNotNull(existingAuthor); + authorRepository.delete(existingAuthor); + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorTest.java new file mode 100644 index 0000000..149c263 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorTest.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +class AuthorTest { + + @Autowired + private AuthorRepository authorRepository; + + @Test + void throwExceptionWhenPlayerSavedWithEmptyName() { + Author author = new Author(); + assertThrows(TransactionSystemException.class, () -> { + authorRepository.save(author); + }); + } + + @Test + void throwExceptionWhenPlayerSavedWithExistingName() { + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + Author existingAuthor = new Author(); + existingAuthor.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + existingAuthor.setLastName(TestConstants.AUTHOR_LASTNAME); + existingAuthor = authorRepository.save(existingAuthor); + assertEquals(TestConstants.AUTHOR_COUNT+1, authorRepository.findAll().size()); + Author author = new Author(); + author.setFirstName(existingAuthor.getFirstName()); + author.setLastName(existingAuthor.getLastName()); + assertThrows(DataIntegrityViolationException.class, () -> { + authorRepository.save(author); + }); + assertNotNull(existingAuthor); + authorRepository.delete(existingAuthor); + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepositoryTest.java new file mode 100644 index 0000000..4df5fd9 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepositoryTest.java @@ -0,0 +1,94 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@Disabled +class BookAuthorRepositoryTest { + + @Autowired + BookRepository bookRepository; + + @Autowired + AuthorRepository authorRepository; + + @Autowired + BookAuthorRepository bookAuthorRepository; + + @Autowired + BookshelfPublisherRepository bookshelfPublisherRepository; + + @Test + @Order(1) + void saveBookAuthor() { + Author author = new Author(); + author.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + author.setLastName(TestConstants.AUTHOR_LASTNAME); + Author savedAuthor = authorRepository.save(author); + assertEquals(TestConstants.AUTHOR_COUNT+1, authorRepository.findAll().size()); + BookshelfPublisher publisher = new BookshelfPublisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + BookshelfPublisher savedPublisher = bookshelfPublisherRepository.save(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT+1, bookshelfPublisherRepository.findAll().size()); + Book book = new Book(); + book.setTitle(TestConstants.ARTICLE_TITLE); + book.setIsbn(TestConstants.BOOK_ISBN); + book.setPublisher(savedPublisher); + Book savedBook = bookRepository.save(book); + assertEquals(TestConstants.BOOK_COUNT+1, bookRepository.findAll().size()); + BookAuthor bookAuthor = new BookAuthor(); + bookAuthor.setBook(savedBook); + bookAuthor.setAuthor(savedAuthor); + BookAuthor savedBookAuthor = bookAuthorRepository.save(bookAuthor); + assertEquals(TestConstants.BOOKAUTHOR_COUNT+1, bookAuthorRepository.findAll().size()); + } + + @Test + @Order(2) + void testFindByBook() { + List bookAuthors = bookAuthorRepository.findAll(); + assertEquals(1, bookAuthors.size()); + Book book = bookAuthors.get(0).getBook(); + assertEquals(1, bookAuthorRepository.findByBook(book).size()); + } + + @Test + @Order(3) + void testFindByAuthor() { + List bookAuthors = bookAuthorRepository.findAll(); + assertEquals(1, bookAuthors.size()); + Author author = bookAuthors.get(0).getAuthor(); + assertEquals(1, bookAuthorRepository.findByAuthor(author).size()); + } + + @Test + @Order(4) + void deleteBookAuthor() { + BookAuthor bookAuthor = bookAuthorRepository.findAll().get(0); + Author author = bookAuthor.getAuthor(); + author.getBookAuthors().remove(bookAuthor); + author = authorRepository.save(author); + Book book = bookAuthor.getBook(); + book.getAuthors().remove(bookAuthor); + BookshelfPublisher publisher = book.getPublisher(); + publisher.getBooks().remove(book); + bookshelfPublisherRepository.save(publisher); + book = bookRepository.save(book); + bookshelfPublisherRepository.delete(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT, bookshelfPublisherRepository.findAll().size()); + bookAuthorRepository.delete(bookAuthor); + assertEquals(TestConstants.BOOKAUTHOR_COUNT, bookAuthorRepository.findAll().size()); + authorRepository.delete(author); + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + bookRepository.delete(book); + assertEquals(TestConstants.BOOK_COUNT, bookRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorTest.java new file mode 100644 index 0000000..dc79341 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorTest.java @@ -0,0 +1,82 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +@Disabled +class BookAuthorTest { + + @Autowired + BookRepository bookRepository; + + @Autowired + AuthorRepository authorRepository; + + @Autowired + BookAuthorRepository bookAuthorRepository; + + @Autowired + BookshelfPublisherRepository bookshelfPublisherRepository; + + @Test + void checkInitialLoad() { + assertEquals(TestConstants.BOOKAUTHOR_COUNT, bookAuthorRepository.findAll().size()); + } + + @Test + void exceptionThrownWhenSavingEmptyValues() { + BookAuthor bookAuthor = new BookAuthor(); + assertThrows(TransactionSystemException.class, () -> { + bookAuthorRepository.save(bookAuthor); + }); + } + + @Test + void exceptionThrownWhenSavingIdenticalEntry() { + Author author = new Author(); + author.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + author.setLastName(TestConstants.AUTHOR_LASTNAME); + Author savedAuthor = authorRepository.save(author); + BookshelfPublisher publisher = new BookshelfPublisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + BookshelfPublisher savedPublisher = bookshelfPublisherRepository.save(publisher); + Book book = new Book(); + book.setTitle(TestConstants.BOOK_TITLE); + book.setIsbn(TestConstants.BOOK_ISBN); + book.setPublisher(publisher); + Book savedBook = bookRepository.save(book); + BookAuthor bookAuthor = new BookAuthor(); + bookAuthor.setBook(book); + bookAuthor.setAuthor(author); + BookAuthor savedBookAuthor = bookAuthorRepository.save(bookAuthor); + BookAuthor bookAuthor1 = new BookAuthor(); + bookAuthor1.setBook(book); + bookAuthor1.setAuthor(author); + assertThrows(DataIntegrityViolationException.class, () -> { + bookAuthorRepository.save(bookAuthor1); + }); + savedAuthor.getBookAuthors().remove(savedBookAuthor); + authorRepository.save(savedAuthor); + savedBook.getAuthors().remove(savedBookAuthor); + bookRepository.save(savedBook); + savedPublisher.getBooks().remove(savedBook); + bookshelfPublisherRepository.save(savedPublisher); + authorRepository.delete(savedAuthor); + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + bookRepository.delete(savedBook); + assertEquals(TestConstants.ARTICLE_COUNT, bookRepository.findAll().size()); + bookshelfPublisherRepository.delete(savedPublisher); + assertEquals(TestConstants.PUBLISHER_COUNT, bookshelfPublisherRepository.findAll().size()); + bookAuthorRepository.delete(savedBookAuthor); + assertEquals(TestConstants.ARTICLEAUTHOR_COUNT, bookAuthorRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookRepositoryTest.java new file mode 100644 index 0000000..969d465 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookRepositoryTest.java @@ -0,0 +1,87 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@Disabled +class BookRepositoryTest { + + @Autowired + BookRepository bookRepository; + + @Autowired + BookshelfPublisherRepository bookshelfPublisherRepository; + + @Test + @Order(1) + void checkInitialLoad(){ + assertEquals(TestConstants.BOOK_COUNT, bookRepository.findAll().size()); + } + + @Test + @Order(2) + void saveBook() { + BookshelfPublisher publisher = new BookshelfPublisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + bookshelfPublisherRepository.save(publisher); + Book book = new Book(); + book.setTitle(TestConstants.BOOK_TITLE); + book.setIsbn(TestConstants.BOOK_ISBN); + book.setPublisher(publisher); + bookRepository.save(book); + assertEquals(TestConstants.BOOK_COUNT+1, bookRepository.findAll().size()); + } + + @Test + @Order(3) + void search() { + List books = bookRepository.search(TestConstants.BOOK_TITLE.substring(2,5)); + assertEquals(1, books.size()); + assertEquals(1, bookRepository.search(TestConstants.BOOK_ISBN.substring(3,7)).size()); + assertEquals(0, bookRepository.search("Article").size()); + } + + @Test + @Order(4) + void findByTitle() { + List books = bookRepository.findByTitle(TestConstants.BOOK_TITLE); + assertEquals(1, books.size()); + assertEquals(TestConstants.BOOK_ISBN, books.get(0).getIsbn()); + } + + @Test + @Order(5) + void findByTitleIgnoreCase() { + List books = bookRepository.findByTitleIgnoreCase(TestConstants.BOOK_TITLE.toLowerCase()); + assertEquals(1, books.size()); + assertEquals(TestConstants.BOOK_ISBN, books.get(0).getIsbn()); + } + + @Test + @Order(6) + void findByIsbn() { + List books = bookRepository.findByIsbn(TestConstants.BOOK_ISBN); + assertEquals(1, books.size()); + assertEquals(TestConstants.BOOK_TITLE, books.get(0).getTitle()); + } + + @Test + @Order(7) + void deleteBook() { + List books = bookRepository.findByIsbn(TestConstants.BOOK_ISBN); + Book book = books.get(0); + BookshelfPublisher publisher = book.getPublisher(); + bookshelfPublisherRepository.delete(publisher); + bookRepository.delete(book); + assertEquals(TestConstants.BOOK_COUNT, bookRepository.findAll().size()); + assertEquals(TestConstants.PUBLISHER_COUNT, bookshelfPublisherRepository.findAll().size()); + } +} \ No newline at end of file diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookTest.java new file mode 100644 index 0000000..0ac3324 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookTest.java @@ -0,0 +1,56 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import de.thpeetz.kontor.comics.data.Publisher; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.orm.jpa.JpaSystemException; +import org.springframework.transaction.TransactionSystemException; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +class BookTest { + + @Autowired + BookRepository bookRepository; + + @Autowired + BookshelfPublisherRepository bookshelfPublisherRepository; + + @Test + void checkInitialLoad() { + assertEquals(TestConstants.BOOK_COUNT, bookRepository.findAll().size()); + } + + @Test + void exceptionThrownWhenSavingEmptyFields() { + Book book = new Book(); + assertThrows(TransactionSystemException.class, () -> { + bookRepository.save(book); + }); + } + + @Test + void exceptionThrownWhenSavingIdenticalISBN() { + BookshelfPublisher publisher = new BookshelfPublisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + BookshelfPublisher savedPublisher = bookshelfPublisherRepository.save(publisher); + Book book = new Book(); + book.setTitle(TestConstants.BOOK_TITLE); + book.setIsbn(TestConstants.BOOK_ISBN); + book.setPublisher(savedPublisher); + Book savedBook = bookRepository.save(book); + Book book1 = new Book(); + book1.setTitle(book.getTitle()); + book1.setIsbn(book.getIsbn()); + assertThrows(TransactionSystemException.class, ()-> { + bookRepository.save(book1); + }); + bookRepository.delete(book); + assertEquals(TestConstants.BOOK_COUNT, bookRepository.findAll().size()); + bookshelfPublisherRepository.delete(savedPublisher); + assertEquals(TestConstants.PUBLISHER_COUNT, bookshelfPublisherRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepositoryTest.java new file mode 100644 index 0000000..98b60fa --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepositoryTest.java @@ -0,0 +1,72 @@ +package de.thpeetz.kontor.bookshelf.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class BookshelfPublisherRepositoryTest { + + @Autowired + private BookshelfPublisherRepository publisherRepository; + + @Test + @Order(1) + void checkInitialLoad() { + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } + + @Test + @Order(2) + void savePublisher() { + BookshelfPublisher publisher = new BookshelfPublisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + publisherRepository.save(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT + 1, publisherRepository.findAll().size()); + } + + @Test + @Order(3) + void findPublisherByName() { + assertEquals(TestConstants.PUBLISHER_COUNT + 1, publisherRepository.findAll().size()); + log.info("Liste der Publisher: {}", publisherRepository.findAll()); + BookshelfPublisher publisher = publisherRepository.findByName(TestConstants.PUBLISHER_NAME); + assertNotNull(publisher); + assertEquals(TestConstants.PUBLISHER_NAME, publisher.getName()); + BookshelfPublisher notFound = publisherRepository.findByName(TestConstants.PUBLISHER_SEARCH); + assertNull(notFound); + List publishers = publisherRepository + .findByNameIgnoreCase(TestConstants.PUBLISHER_NAME.toLowerCase()); + assertEquals(1, publishers.size()); + assertEquals(TestConstants.PUBLISHER_NAME, publishers.get(0).getName()); + } + + @Test + @Order(4) + void searchPublisher() { + List publishers = publisherRepository.search(TestConstants.PUBLISHER_NAME.substring(2, 6)); + assertEquals(1, publishers.size()); + assertEquals(TestConstants.PUBLISHER_NAME, publishers.get(0).getName()); + } + + @Test + @Order(5) + void deletePublisher() { + BookshelfPublisher publisher = publisherRepository.findByName(TestConstants.PUBLISHER_NAME); + assertNotNull(publisher); + publisherRepository.delete(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherTest.java new file mode 100644 index 0000000..7a73ae1 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherTest.java @@ -0,0 +1,45 @@ +package de.thpeetz.kontor.bookshelf.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.bookshelf.TestConstants; + +@SpringBootTest +class BookshelfPublisherTest { + + @Autowired + private BookshelfPublisherRepository publisherRepository; + + @Test + void checkInitialDataLoad() { + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } + + @Test + void throwExceptionWhenPublisherSavedWithEmptyName() { + BookshelfPublisher publisher = new BookshelfPublisher(); + assertThrows(TransactionSystemException.class, () -> { + publisherRepository.save(publisher); + }); + } + + @Test + void savePublisherWithIdenticalName() { + BookshelfPublisher publisher1 = new BookshelfPublisher(); + publisher1.setName(TestConstants.PUBLISHER_NAME); + publisherRepository.save(publisher1); + BookshelfPublisher publisher2 = new BookshelfPublisher(); + publisher2.setName(TestConstants.PUBLISHER_NAME); + assertThrows(DataIntegrityViolationException.class, () -> { + publisherRepository.save(publisher2); + }); + publisherRepository.delete(publisher1); + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/bookshelf/services/BookshelfServiceTest.java b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/services/BookshelfServiceTest.java new file mode 100644 index 0000000..b78a899 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/bookshelf/services/BookshelfServiceTest.java @@ -0,0 +1,102 @@ +package de.thpeetz.kontor.bookshelf.services; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import de.thpeetz.kontor.bookshelf.data.Author; +import de.thpeetz.kontor.bookshelf.data.Book; +import de.thpeetz.kontor.bookshelf.data.BookshelfPublisher; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class BookshelfServiceTest { + + @Autowired + private BookshelfService bookshelfService; + + @Test + @Order(1) + void testFindAllPublishers() { + List publishers = bookshelfService.findAllPublishers(null); + assertEquals(TestConstants.PUBLISHER_COUNT, publishers.size()); + } + + @Test + @Order(2) + void testSavePublisher() { + BookshelfPublisher publisher = new BookshelfPublisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + bookshelfService.savePublisher(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT + 1, bookshelfService.findAllPublishers(null).size()); + } + + @Test + @Order(3) + void testFindPublisherByName() { + assertEquals(TestConstants.PUBLISHER_COUNT + 1, bookshelfService.findAllPublishers(null).size()); + BookshelfPublisher publisher = bookshelfService.findPublisherByName(TestConstants.PUBLISHER_NAME); + assertNotNull(publisher); + } + + @Test + @Order(4) + void testDeletePublisher() { + List publishers = bookshelfService.findAllPublishers(TestConstants.PUBLISHER_NAME); + assertEquals(1, publishers.size()); + bookshelfService.deletePublisher(publishers.get(0)); + assertEquals(TestConstants.PUBLISHER_COUNT, bookshelfService.findAllPublishers(null).size()); + } + + @Test + @Order(5) + void testFindAllAuthors() { + assertEquals(TestConstants.AUTHOR_COUNT, bookshelfService.findAllAuthors(null).size()); + } + + @Test + @Order(6) + void testSaveAuthor() { + Author author = new Author(); + author.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + author.setLastName(TestConstants.AUTHOR_LASTNAME); + bookshelfService.saveAuthor(author); + assertEquals(TestConstants.AUTHOR_COUNT+1, bookshelfService.findAllAuthors(null).size()); + } + + @Test + @Order(7) + void testDeleteAuthor() { + List authors = bookshelfService.findAllAuthors(TestConstants.AUTHOR_FIRSTNAME); + assertEquals(1, authors.size()); + bookshelfService.deleteAuthor(authors.get(0)); + assertEquals(TestConstants.AUTHOR_COUNT, bookshelfService.findAllAuthors(null).size()); + } + + @Test + @Order(8) + void findAllBooks() { + assertEquals(TestConstants.BOOK_COUNT, bookshelfService.findAllBooks(null).size()); + } + + @Test + @Order(9) + void saveBook() { + assertEquals(TestConstants.BOOK_COUNT, bookshelfService.findAllBooks(null).size()); + } + + @Test + @Order(10) + void deleteBook() { + List books = bookshelfService.findAllBooks(null); + assertEquals(0, books.size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/ComicConstantsTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/ComicConstantsTest.java new file mode 100644 index 0000000..b1e50ce --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/ComicConstantsTest.java @@ -0,0 +1,14 @@ +package de.thpeetz.kontor.comics; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class ComicConstantsTest { + + @Test + void getArtistConstants() { + assertEquals("Artist", ComicConstants.ARTIST); + assertEquals("comics/artist", ComicConstants.ARTIST_ROUTE); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/TestConstants.java b/springboot/src/test/java/de/thpeetz/kontor/comics/TestConstants.java new file mode 100644 index 0000000..b77f55d --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/TestConstants.java @@ -0,0 +1,67 @@ +package de.thpeetz.kontor.comics; + +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.Publisher; +import de.thpeetz.kontor.comics.data.Worktype; +import de.thpeetz.kontor.comics.services.ComicService; + +public class TestConstants { + public static final String ARTIST_NAME = "Lastname, Firstname"; + public static final String COMIC_TITLE = "TestComic"; + public static final String WORKTYPE_INKER = "Inker"; + public static final String ISSUE_ISSUENUMBER = "Issuenumber"; + public static final String PUBLISHER_NAME = "Publisher"; + public static final String SOJOURN_TPB_COMIC_TITLE = "Sojourn"; + public static final String SOJOURN_TPB_NAME = "The Dragons Tale"; + public static final String STORYARC_NAME = "StoryArc"; + public static final String TRADEPAPERBACK_NAME = "TradePaperback"; + public static final String VOLUME_NAME = "Volume"; + public static final String WORKTYPE_NAME = "Worktype"; + public static final int ARTIST_COUNT = 5; + public static final int BATTLE_POPE_ISSUE_COUNT = 12; + public static final int COMIC_COUNT = 169; + public static final int COMICWORK_COUNT = 18; + public static final int ISSUE_COUNT = 750; + public static final int MARVEL_COMIC_COUNT = 50; + public static final int PUBLISHER_COUNT = 18; + public static final int SOJOURN_TPB_COUNT = 4; + public static final int SOJOURN_TPB_START = 7; + public static final int SOJOURN_TPB_END = 12; + public static final int STORYARC_COUNT = 3; + public static final int TRADEPAPERBACK_COUNT = 40; + public static final int VOLUME_COUNT = 0; + public static final int WORKTYPE_COUNT = 3; + + public static Comic getComicWithStoryArcs(ComicService service) { + return service.findComicByTitle("Emma Frost"); + } + + public static Comic getComicWithTradePaperbacks(ComicService service) { + return service.findComicByTitle(SOJOURN_TPB_COMIC_TITLE); + } + + public static Comic getComicWithIssues(ComicService service) { + return service.findComicByTitle("Battle Pope"); + } + + public static Comic getComicWithoutReferences(ComicService service) { + return service.findComicByTitle("Gen13"); + } + + public static Artist getArtistWithoutReferences(ComicService service) { + return service.findArtistByName("Marz, Ron"); + } + + public static Worktype getWorktypeWithoutReferences(ComicService service) { + return service.findWorktypeByName(WORKTYPE_INKER); + } + + public static Publisher getPublisherForComicTests(ComicService service) { + return service.findPublisherByName("Marvel"); + } + + public static Publisher getAnotherPublisherForComicTests(ComicService service) { + return service.findPublisherByName("DC"); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/ArtistRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ArtistRepositoryTest.java new file mode 100644 index 0000000..96d53f8 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ArtistRepositoryTest.java @@ -0,0 +1,73 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class ArtistRepositoryTest { + + private Artist stanLee; + private static final String ARTISTNAME = "Lee, Stan"; + + @Autowired + ArtistRepository artistRepository; + + @BeforeEach + void setupData() { + stanLee = new Artist(); + stanLee.setName(ARTISTNAME); + } + + @Test + @Order(1) + void checkInitialLoad() { + int count = artistRepository.findAll().size(); + assertEquals(5, count); + } + + @Test + @Order(2) + void saveArtist() { + int count = artistRepository.findAll().size(); + artistRepository.save(stanLee); + assertEquals(count+1, artistRepository.findAll().size()); + } + + @Test + @Order(3) + void findArtist() { + List artists = artistRepository.findByNameIgnoreCase("lEE, sTAN"); + assertTrue(artists.size() > 0); + assertEquals(artists.get(0).getName(), stanLee.getName()); + } + + @Test + @Order(4) + void searchArtist() { + List artists = artistRepository.search("Lee"); + assertEquals(1, artists.size()); + assertEquals(ARTISTNAME, artists.get(0).getName()); + List artists2 = artistRepository.search("Stan"); + assertEquals(1, artists2.size()); + assertEquals(ARTISTNAME, artists2.get(0).getName()); + } + + @Test + @Order(5) + void deleteArtist() { + int count = artistRepository.findAll().size(); + Artist artist = artistRepository.findByName(ARTISTNAME); + artistRepository.delete(artist); + assertEquals(count-1, artistRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/ArtistTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ArtistTest.java new file mode 100644 index 0000000..855795e --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ArtistTest.java @@ -0,0 +1,48 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; + +@SpringBootTest +class ArtistTest { + + @Autowired + private ArtistRepository artistRepository; + + @Test + void checkInitialDataLoad() { + List artists = artistRepository.findAll(); + assertEquals(TestConstants.ARTIST_COUNT, artists.size()); + } + + @Test + void throwExceptionWhenArtistSavedWithEmptyName() { + Artist artist1 = new Artist(); + assertThrows(TransactionSystemException.class, () -> { + artistRepository.save(artist1); + }); + } + + @Test + void saveArtistWithIdenticalName() { + String artistName = "Lastname, Firstname"; + Artist artist1 = new Artist(); + artist1.setName(artistName); + artistRepository.save(artist1); + Artist artist2 = new Artist(); + artist2.setName(artistName); + assertThrows(DataIntegrityViolationException.class, () -> { + artistRepository.save(artist2); + }); + artistRepository.delete(artist1); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicRepositoryTest.java new file mode 100644 index 0000000..70c0d54 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicRepositoryTest.java @@ -0,0 +1,53 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ComicRepositoryTest { + + @Autowired + private ComicRepository comicRepository; + + @Autowired + private PublisherRepository publisherRepository; + + @Test + void testFindByTitle() { + List comics = comicRepository.findByTitle("Emma Frost"); + assertEquals(1, comics.size()); + assertEquals("Emma Frost", comics.get(0).getTitle()); + Publisher publisher = comics.get(0).getPublisher(); + assertEquals("Marvel", publisher.getName()); + } + + @Test + void testFindByTitleIgnoreCase() { + List comics = comicRepository.findByTitleIgnoreCase("x-men"); + assertEquals(1, comics.size()); + assertEquals("X-Men", comics.get(0).getTitle()); + } + @Test + void testFindByTitleAndPublisher() { + Publisher publisher = publisherRepository.findByName("Marvel"); + assertNotNull(publisher); + Comic found = comicRepository.findByTitleAndPublisher("Emma Frost", publisher); + assertNotNull(found); + assertEquals("Emma Frost", found.getTitle()); + assertEquals("Marvel", found.getPublisher().getName()); + + } + + @Test + void testSearch() { + List comics = comicRepository.search("X-men"); + assertEquals(11, comics.size()); + assertTrue(comics.stream().map(comic -> comic.getTitle()).collect(Collectors.toList()).contains("Astonishing X-Men")); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicTest.java new file mode 100644 index 0000000..5560346 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicTest.java @@ -0,0 +1,77 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.orm.jpa.JpaSystemException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class ComicTest { + + @Autowired + private ComicRepository comicRepository; + + @Autowired + private ComicService comicService; + + @Test + @Order(1) + void checkInitialDataLoad() { + List comics = comicRepository.findAll(); + assertEquals(TestConstants.COMIC_COUNT, comics.size()); + } + + @Test + @Order(2) + void exceptionThrownWhenSavingComicWithoutPublisher() { + Comic comic = new Comic(); + comic.setTitle(TestConstants.COMIC_TITLE); + assertThrows(TransactionSystemException.class, () -> { + comicRepository.save(comic); + }); + } + + @Test + @Order(3) + void exceptionThrownWhenSavingComicWithEmptyTitle() { + Comic comic = new Comic(); + Publisher publisher = TestConstants.getPublisherForComicTests(comicService); + comic.setPublisher(publisher); + assertNull(comic.getTitle()); + assertThrows(TransactionSystemException.class, () -> { + comicRepository.save(comic); + }); + comic.setTitle(""); + assertTrue(comic.getTitle().isEmpty()); + assertThrows(TransactionSystemException.class, () -> { + comicRepository.save(comic); + }); + } + + @Test + @Order(4) + void exceptionThrownWhenSavingComicWithSameNameFromDifferentPublishers() { + Publisher publisher = TestConstants.getPublisherForComicTests(comicService); + Comic comic = new Comic(); + comic.setTitle(TestConstants.SOJOURN_TPB_COMIC_TITLE); + comic.setPublisher(publisher); + assertThrows(DataIntegrityViolationException.class, () -> { + comicRepository.save(comic); + }); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkRepositoryTest.java new file mode 100644 index 0000000..0902b84 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkRepositoryTest.java @@ -0,0 +1,83 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class ComicWorkRepositoryTest { + + @Autowired + private ComicWorkRepository comicWorkRepository; + + @Autowired + private ComicRepository comicRepository; + + @Autowired + private WorktypeRepository worktypeRepository; + + @Autowired + private ArtistRepository artistRepository; + + @Autowired + private ComicService comicService; + + @Test + @Order(1) + void checkInitialLoad() { + int count = comicWorkRepository.findAll().size(); + assertEquals(18, count); + } + + @Test + @Order(2) + void saveComicWork() { + int count = comicWorkRepository.findAll().size(); + Artist artist = artistRepository.findByName("Turner, Michael"); + Worktype worktype = worktypeRepository.findByName("Writer"); + Comic comic = comicRepository.findByTitle("Emma Frost").get(0); + ComicWork comicWork = new ComicWork(); + comicWork.setArtist(artist); + comicWork.setComic(comic); + comicWork.setWorkType(worktype); + comicWorkRepository.save(comicWork); + assertEquals(count + 1, comicWorkRepository.findAll().size()); + } + + @Test + @Order(3) + void findByComicAndArtistAndComicWork() { + assertEquals(19, comicWorkRepository.count()); + Artist artist = artistRepository.findByName("Turner, Michael"); + Worktype worktype = worktypeRepository.findByName("Writer"); + Comic comic = comicRepository.findByTitle("Emma Frost").get(0); + ComicWork comicWork = comicWorkRepository.findbyComicAndArtistAndWorktype(comic, artist, worktype); + assertNotNull(comicWork); + assertEquals(comicWork.getArtist().getName(), artist.getName()); + assertEquals(comicWork.getComic().getTitle(), comic.getTitle()); + assertEquals(comicWork.getWorkType().getName(), worktype.getName()); + ComicWork notFound = comicWorkRepository.findbyComicAndArtistAndWorktype(comic, artist, null); + assertNull(notFound); + } + + @Test + @Order(4) + void deleteComicWork() { + long count = comicWorkRepository.count(); + Artist artist = artistRepository.findByName("Turner, Michael"); + Worktype worktype = worktypeRepository.findByName("Writer"); + Comic comic = comicRepository.findByTitle("Emma Frost").get(0); + ComicWork comicWork = comicWorkRepository.findbyComicAndArtistAndWorktype(comic, artist, worktype); + assertNotNull(comicWork); + comicService.deleteComicWork(comicWork); + assertEquals(count - 1, comicWorkRepository.count()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkTest.java new file mode 100644 index 0000000..ac2eac2 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkTest.java @@ -0,0 +1,24 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +public class ComicWorkTest { + + @Autowired + private ComicService comicService; + + @Test + void checkInitialDataLoad() { + List comicWorks = comicService.findAllComicWorks(); + assertEquals(18, comicWorks.size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/IssueRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/IssueRepositoryTest.java new file mode 100644 index 0000000..8d3be93 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/IssueRepositoryTest.java @@ -0,0 +1,45 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class IssueRepositoryTest { + + @Autowired + private IssueRepository issueRepository; + + @Autowired + private ComicService comicService; + + @Test + void testFindByComic() { + Comic comic = TestConstants.getComicWithIssues(comicService); + List issues = issueRepository.findByComic(comic); + assertEquals(TestConstants.BATTLE_POPE_ISSUE_COUNT, issues.size()); + } + + @Test + void testFindByComicAndIssueNumber() { + Comic comic = TestConstants.getComicWithIssues(comicService); + Issue issue = issueRepository.findByComicAndIssueNumber(comic, "12"); + assertNotNull(issue); + assertFalse(issue.getIsRead()); + } + + @Test + void testSearch() { + List issues = issueRepository.search("2"); + assertEquals(187, issues.size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/IssueTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/IssueTest.java new file mode 100644 index 0000000..1760fd1 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/IssueTest.java @@ -0,0 +1,62 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class IssueTest { + + @Autowired + private IssueRepository issueRepository; + + @Autowired + private ComicService comicService; + + @Test + void checkInitialDataLoad() { + List issues = comicService.findAllIssues(); + assertEquals(TestConstants.ISSUE_COUNT, issues.size()); + } + + @Test + void exceptionThrownWhenSavingIssueWithEmptyNumber() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertNotNull(comic); + Issue issue = new Issue(); + issue.setComic(comic); + assertThrows(TransactionSystemException.class, () -> { + issueRepository.save(issue); + }); + } + + @Test + void exceptionThrownWhenSavingStoryArcWithoutComic() { + Issue issue = new Issue(); + issue.setIssueNumber(TestConstants.ISSUE_ISSUENUMBER); + assertThrows(TransactionSystemException.class, () -> { + issueRepository.save(issue); + }); + } + + @Test + void saveStoryArc() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertEquals(0, comic.getStoryArcs().size()); + Issue issue = new Issue(); + issue.setComic(comic); + issue.setIssueNumber(TestConstants.ISSUE_ISSUENUMBER); + Issue savedInstance = issueRepository.save(issue); + assertNotNull(savedInstance); + comicService.deleteIssue(savedInstance); + assertEquals(0, comic.getStoryArcs().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/PublisherRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/PublisherRepositoryTest.java new file mode 100644 index 0000000..50175b4 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/PublisherRepositoryTest.java @@ -0,0 +1,68 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class PublisherRepositoryTest { + + @Autowired + private PublisherRepository publisherRepository; + + @Test + @Order(1) + void checkInitialLoad() { + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } + + @Test + @Order(2) + void savePublisher() { + Publisher publisher = new Publisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + publisherRepository.save(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT + 1, publisherRepository.findAll().size()); + } + + @Test + @Order(3) + void findPublisherByName() { + Publisher publisher = publisherRepository.findByName(TestConstants.PUBLISHER_NAME); + assertNotNull(publisher); + assertEquals(TestConstants.PUBLISHER_NAME, publisher.getName()); + Publisher notFound = publisherRepository.findByName("Cow"); + assertNull(notFound); + List publishers = publisherRepository + .findByNameIgnoreCase(TestConstants.PUBLISHER_NAME.toLowerCase()); + assertEquals(1, publishers.size()); + assertEquals(TestConstants.PUBLISHER_NAME, publishers.get(0).getName()); + } + + @Test + @Order(4) + void searchPublisher() { + List publishers = publisherRepository.search("Cow"); + assertEquals(1, publishers.size()); + assertEquals("Top Cow Productions", publishers.get(0).getName()); + } + + @Test + @Order(5) + void deletePublisher() { + Publisher publisher = publisherRepository.findByName(TestConstants.PUBLISHER_NAME); + assertNotNull(publisher); + publisherRepository.delete(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/PublisherTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/PublisherTest.java new file mode 100644 index 0000000..264a1d0 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/PublisherTest.java @@ -0,0 +1,45 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; + +@SpringBootTest +class PublisherTest { + + @Autowired + private PublisherRepository publisherRepository; + + @Test + void checkInitialDataLoad() { + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } + + @Test + void throwExceptionWhenPublisherSavedWithEmptyName() { + Publisher publisher = new Publisher(); + assertThrows(TransactionSystemException.class, () -> { + publisherRepository.save(publisher); + }); + } + + @Test + void savePublisherWithIdenticalName() { + Publisher publisher1 = new Publisher(); + publisher1.setName(TestConstants.PUBLISHER_NAME); + publisherRepository.save(publisher1); + Publisher publisher2 = new Publisher(); + publisher2.setName(TestConstants.PUBLISHER_NAME); + assertThrows(DataIntegrityViolationException.class, () -> { + publisherRepository.save(publisher2); + }); + publisherRepository.delete(publisher1); + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/StoryArcRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/StoryArcRepositoryTest.java new file mode 100644 index 0000000..52bad7e --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/StoryArcRepositoryTest.java @@ -0,0 +1,45 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class StoryArcRepositoryTest { + + @Autowired + private StoryArcRepository storyArcRepository; + + @Autowired + private ComicService comicService; + + @Test + void testSearch() { + List storyArcs = storyArcRepository.search("Learn"); + assertNotNull(storyArcs); + assertEquals(1, storyArcs.size()); + } + + @Test + void findByComic() { + Comic comic = TestConstants.getComicWithStoryArcs(comicService); + List storyArcs = storyArcRepository.findByComic(comic); + assertNotNull(storyArcs); + assertEquals(3, storyArcs.size()); + } + + @Test + void findByNameAndComic() { + Comic comic = TestConstants.getComicWithStoryArcs(comicService); + StoryArc storyArc = storyArcRepository.findByNameAndComic("Higher Learning", comic); + assertNotNull(storyArc); + assertEquals("Emma Frost", storyArc.getComic().getTitle()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/StoryArcTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/StoryArcTest.java new file mode 100644 index 0000000..07134ca --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/StoryArcTest.java @@ -0,0 +1,61 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class StoryArcTest { + + @Autowired + private StoryArcRepository storyArcRepository; + + @Autowired + private ComicService comicService; + + @Test + void checkInitialLoad() { + assertEquals(TestConstants.STORYARC_COUNT, storyArcRepository.findAll().size()); + } + + @Test + void exceptionThrownWhenSavingStoryArcWithEmptyName() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertNotNull(comic); + StoryArc storyArc = new StoryArc(); + storyArc.setComic(comic); + assertThrows(TransactionSystemException.class, () -> { + storyArcRepository.save(storyArc); + }); + } + + @Test + void exceptionThrownWhenSavingStoryArcWithoutComic() { + StoryArc storyArc = new StoryArc(); + storyArc.setName(TestConstants.STORYARC_NAME); + assertThrows(TransactionSystemException.class, () -> { + storyArcRepository.save(storyArc); + }); + } + + @Test + void saveStoryArc() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertEquals(0, comic.getStoryArcs().size()); + StoryArc storyArc = new StoryArc(); + storyArc.setName(TestConstants.STORYARC_NAME); + storyArc.setComic(comic); + StoryArc savedInstance = storyArcRepository.save(storyArc); + assertNotNull(savedInstance); + comicService.deleteStoryArc(savedInstance); + assertEquals(0, comic.getStoryArcs().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackRepositoryTest.java new file mode 100644 index 0000000..2c66c12 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackRepositoryTest.java @@ -0,0 +1,59 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class TradePaperbackRepositoryTest { + + @Autowired + private TradePaperbackRepository tradePaperbackRepository; + + @Autowired + private ComicService comicService; + + @Test + void testSearch() { + List tradePaperbacks = tradePaperbackRepository.search("Dragon"); + assertNotNull(tradePaperbacks); + assertEquals(1, tradePaperbacks.size()); + } + + @Test + void testFindByComic() { + Comic comic = TestConstants.getComicWithTradePaperbacks(comicService); + List tradePaperbacks = tradePaperbackRepository.findByComic(comic); + assertNotNull(tradePaperbacks); + assertEquals(TestConstants.SOJOURN_TPB_COUNT, tradePaperbacks.size()); + } + + @Test + void testFindByNameAndComic() { + Comic comic = TestConstants.getComicWithTradePaperbacks(comicService); + List tradePaperbacks = tradePaperbackRepository + .findByNameAndComic(TestConstants.SOJOURN_TPB_NAME, comic); + assertNotNull(tradePaperbacks); + assertTrue(tradePaperbacks.size() > 0); + assertEquals(TestConstants.SOJOURN_TPB_COMIC_TITLE, tradePaperbacks.get(0).getComic().getTitle()); + } + + @Test + void testFindByFields() { + Comic comic = TestConstants.getComicWithTradePaperbacks(comicService); + TradePaperback tradePaperback = tradePaperbackRepository.findByFields(TestConstants.SOJOURN_TPB_NAME, + comic, TestConstants.SOJOURN_TPB_START, TestConstants.SOJOURN_TPB_END); + assertNotNull(tradePaperback); + assertEquals(TestConstants.SOJOURN_TPB_COMIC_TITLE, tradePaperback.getComic().getTitle()); + assertEquals(TestConstants.SOJOURN_TPB_END, tradePaperback.getIssueEnd()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackTest.java new file mode 100644 index 0000000..61df1d6 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackTest.java @@ -0,0 +1,59 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class TradePaperbackTest { + + @Autowired + private TradePaperbackRepository tradePaperbackRepository; + + @Autowired + private ComicService comicService; + + @Test + void checkInitialDataLoad() { + assertEquals(TestConstants.TRADEPAPERBACK_COUNT, tradePaperbackRepository.findAll().size()); + } + + @Test + void exceptionThrownWhenSavingTradePaperbackWithEmptyName() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertNotNull(comic); + TradePaperback tradePaperback = new TradePaperback(); + tradePaperback.setComic(comic); + assertThrows(TransactionSystemException.class, () -> { + tradePaperbackRepository.save(tradePaperback); + }); + } + + @Test + void exceptionThrownWhenSavingTradePaperbackWithoutComic() { + TradePaperback tradePaperback = new TradePaperback(); + tradePaperback.setName(TestConstants.TRADEPAPERBACK_NAME); + assertThrows(TransactionSystemException.class, () -> { + tradePaperbackRepository.save(tradePaperback); + }); + } + + @Test + void saveTradePaperback() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertEquals(0, comic.getTradePaperbacks().size()); + TradePaperback tradePaperback = new TradePaperback(); + tradePaperback.setName(TestConstants.TRADEPAPERBACK_NAME); + tradePaperback.setComic(comic); + TradePaperback savedInstance = tradePaperbackRepository.save(tradePaperback); + assertNotNull(savedInstance); + comicService.deleteTradePaperBack(savedInstance); + assertEquals(0, comic.getTradePaperbacks().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/VolumeRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/VolumeRepositoryTest.java new file mode 100644 index 0000000..ce04a11 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/VolumeRepositoryTest.java @@ -0,0 +1,61 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class VolumeRepositoryTest { + + @Autowired + private VolumeRepository volumeRepository; + + @Autowired + private ComicRepository comicRepository; + + @Autowired + private ComicService comicService; + + @Test + void testFindByComic() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertEquals(0, comic.getVolumes().size()); + Volume volume = new Volume(); + volume.setName(TestConstants.VOLUME_NAME); + volume.setComic(comic); + Volume savedInstance = volumeRepository.save(volume); + assertNotNull(savedInstance); + + List found = volumeRepository.findByComic(comic); + assertEquals(1, found.size()); + + comicService.deleteVolume(found.get(0)); + assertEquals(0, comic.getVolumes().size()); + assertEquals(TestConstants.VOLUME_COUNT, volumeRepository.count()); + } + + @Test + void testFindByName() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertEquals(0, comic.getVolumes().size()); + Volume volume = new Volume(); + volume.setName(TestConstants.VOLUME_NAME); + volume.setComic(comic); + Volume savedInstance = volumeRepository.save(volume); + assertNotNull(savedInstance); + + List found = volumeRepository.findByName(TestConstants.VOLUME_NAME); + assertEquals(1, found.size()); + + comicService.deleteVolume(found.get(0)); + assertEquals(0, comic.getVolumes().size()); + assertEquals(TestConstants.VOLUME_COUNT, volumeRepository.count()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/VolumeTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/VolumeTest.java new file mode 100644 index 0000000..7189965 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/VolumeTest.java @@ -0,0 +1,64 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class VolumeTest { + + @Autowired + private VolumeRepository volumeRepository; + + @Autowired + private ComicRepository comicRepository; + + @Autowired + private ComicService comicService; + + @Test + void checkInitialDataLoad() { + assertEquals(TestConstants.VOLUME_COUNT, volumeRepository.count()); + } + + @Test + void exceptionThrownWhenSavingVolumeWithEmptyName() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + Volume volume = new Volume(); + volume.setComic(comic); + assertThrows(TransactionSystemException.class, () -> { + volumeRepository.save(volume); + }); + } + + @Test + void exceptionThrownWhenSavingVolumeWithoutComic() { + Volume volume = new Volume(); + volume.setName(TestConstants.VOLUME_NAME); + assertThrows(TransactionSystemException.class, () -> { + volumeRepository.save(volume); + }); + } + + @Test + void saveVolume() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertEquals(0, comic.getVolumes().size()); + Volume volume = new Volume(); + volume.setName(TestConstants.VOLUME_NAME); + volume.setComic(comic); + Volume savedInstance = volumeRepository.save(volume); + assertNotNull(savedInstance); + comicService.deleteVolume(savedInstance); + assertEquals(0, comic.getVolumes().size()); + assertEquals(TestConstants.VOLUME_COUNT, volumeRepository.count()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/WorktypeRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/WorktypeRepositoryTest.java new file mode 100644 index 0000000..5dae5e8 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/WorktypeRepositoryTest.java @@ -0,0 +1,46 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; + +@SpringBootTest +class WorktypeRepositoryTest { + + @Autowired + private WorktypeRepository worktypeRepository; + + @Test + void findWorktypeByName() { + Worktype found = worktypeRepository.findByName("Writer"); + assertNotNull(found); + Worktype notFound = worktypeRepository.findByName("er"); + assertNull(notFound); + } + + @Test + void findWorktypeByNameIgnoreCase() { + List worktypes = worktypeRepository.findByNameIgnoreCase("Writer".toLowerCase()); + assertNotNull(worktypes); + assertEquals(1, worktypes.size()); + assertEquals("Writer", worktypes.get(0).getName()); + } + + @Test + void searchWorktype() { + List worktypes = worktypeRepository.search("er"); + assertEquals(3, worktypes.size()); + assertTrue(worktypes.stream().map(worktype -> worktype.getName()).collect(Collectors.toList()).contains("Writer")); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/data/WorktypeTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/data/WorktypeTest.java new file mode 100644 index 0000000..3145ee9 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/data/WorktypeTest.java @@ -0,0 +1,83 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.orm.jpa.JpaSystemException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class WorktypeTest { + + @Autowired + private WorktypeRepository worktypeRepository; + + @Test + @Order(1) + void checkInitialDataLoad() { + List worktypes = worktypeRepository.findAll(); + assertEquals(3, worktypes.size()); + } + + @Test + @Order(2) + void findWorktypeByName() { + Worktype found = worktypeRepository.findByName(TestConstants.WORKTYPE_INKER); + assertNotNull(found); + assertEquals(TestConstants.WORKTYPE_INKER, found.getName()); + Worktype notFound = worktypeRepository.findByName("er"); + assertNull(notFound); + } + + @Test + @Order(3) + void throwExceptionWhenArtistSavedWithEmptyName() { + Worktype worktype1 = new Worktype(); + assertThrows(TransactionSystemException.class, () -> { + worktypeRepository.save(worktype1); + }); + } + + @Test + @Order(4) + void saveWorktypeWithIdenticalName() { + Worktype worktype1 = new Worktype(); + worktype1.setName(TestConstants.WORKTYPE_NAME); + worktypeRepository.save(worktype1); + Worktype worktype2 = new Worktype(); + worktype2.setName(TestConstants.WORKTYPE_NAME); + assertThrows(DataIntegrityViolationException.class, () -> { + worktypeRepository.save(worktype2); + }); + worktypeRepository.delete(worktype1); + } + + @Test + @Order(5) + void saveWorktype() { + Worktype worktype = new Worktype(); + worktype.setName(TestConstants.WORKTYPE_NAME); + worktypeRepository.save(worktype); + assertEquals(TestConstants.WORKTYPE_COUNT + 1, worktypeRepository.count()); + } + + @Test + @Order(6) + void deleteWorktype() { + Worktype worktype = worktypeRepository.findByName(TestConstants.WORKTYPE_NAME); + assertNotNull(worktype); + worktypeRepository.delete(worktype); + assertEquals(TestConstants.WORKTYPE_COUNT, worktypeRepository.count()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/comics/services/ComicServiceTest.java b/springboot/src/test/java/de/thpeetz/kontor/comics/services/ComicServiceTest.java new file mode 100644 index 0000000..d449298 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/comics/services/ComicServiceTest.java @@ -0,0 +1,350 @@ +package de.thpeetz.kontor.comics.services; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.data.Issue; +import de.thpeetz.kontor.comics.data.Publisher; +import de.thpeetz.kontor.comics.data.StoryArc; +import de.thpeetz.kontor.comics.data.TradePaperback; +import de.thpeetz.kontor.comics.data.Volume; +import de.thpeetz.kontor.comics.data.Worktype; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class ComicServiceTest { + + private static final Logger log = LoggerFactory.getLogger(ComicServiceTest.class); + @Autowired + private ComicService comicService; + + @Test + @Order(1) + void testFindAllArtists() { + assertEquals(TestConstants.ARTIST_COUNT, comicService.findAllArtists(null).size()); + assertEquals(1, comicService.findAllArtists("Turn").size()); + } + + @Test + @Order(1) + void testFindArtistByName() { + assertNull(comicService.findArtistByName("Lee, Stan")); + Artist artist = comicService.findArtistByName("Turner, Michael"); + assertNotNull(artist); + assertNotNull(artist.getComicWorks()); + assertFalse(artist.getComicWorks().isEmpty()); + } + + @Test + @Order(2) + void testSaveArtist() { + Artist artist = new Artist(); + artist.setName(TestConstants.ARTIST_NAME); + comicService.saveArtist(artist); + assertEquals(TestConstants.ARTIST_COUNT + 1, comicService.findAllArtists(null).size()); + } + + @Test + @Order(3) + void testDeleteArtist() { + List artists = comicService.findAllArtists(TestConstants.ARTIST_NAME); + assertEquals(1, artists.size()); + comicService.deleteArtist(artists.get(0)); + assertEquals(TestConstants.ARTIST_COUNT, comicService.findAllArtists(null).size()); + } + + @Test + @Order(4) + void testFindAllPublishers() { + assertEquals(TestConstants.PUBLISHER_COUNT, comicService.findAllPublishers(null).size()); + assertEquals(1, comicService.findAllPublishers("Cow").size()); + } + + @Test + @Order(4) + void testFindPublisherByName() { + Publisher publisher = comicService.findPublisherByName("Marvel"); + assertNotNull(publisher); + assertNotNull(publisher.getComics()); + assertEquals(TestConstants.MARVEL_COMIC_COUNT, publisher.getComics().size()); + assertNull(comicService.findPublisherByName(TestConstants.PUBLISHER_NAME)); + } + + @Test + @Order(5) + void testSavePublisher() { + Publisher publisher = new Publisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + comicService.savePublisher(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT + 1, comicService.findAllPublishers(null).size()); + } + + @Test + @Order(6) + void testDeletePublisher() { + List publishers = comicService.findAllPublishers(TestConstants.PUBLISHER_NAME); + assertEquals(1, publishers.size()); + comicService.deletePublisher(publishers.get(0)); + assertEquals(TestConstants.PUBLISHER_COUNT, comicService.findAllPublishers(null).size()); + } + + @Test + @Order(7) + void testFindAllWorktypes() { + assertEquals(TestConstants.WORKTYPE_COUNT, comicService.findAllWorktypes(null).size()); + assertEquals(1, comicService.findAllWorktypes("ite").size()); + } + + @Test + @Order(7) + void testFindWorktypeByName() { + Worktype worktype = comicService.findWorktypeByName("Writer"); + assertNotNull(worktype); + assertNotNull(worktype.getComicWorks()); + assertFalse(worktype.getComicWorks().isEmpty()); + worktype = comicService.findWorktypeByName("Inker"); + assertNotNull(worktype); + assertNotNull(worktype.getComicWorks()); + assertTrue(worktype.getComicWorks().isEmpty()); + assertNull(comicService.findWorktypeByName(TestConstants.WORKTYPE_NAME)); + } + + @Test + @Order(8) + void testSaveWorktype() { + Worktype worktype = new Worktype(); + worktype.setName(TestConstants.WORKTYPE_NAME); + comicService.saveWorktype(worktype); + assertEquals(TestConstants.WORKTYPE_COUNT + 1, comicService.findAllWorktypes(null).size()); + } + + @Test + @Order(9) + void testDeleteWorktype() { + List worktypes = comicService.findAllWorktypes(TestConstants.WORKTYPE_NAME); + assertEquals(1, worktypes.size()); + comicService.deleteWorktype(worktypes.get(0)); + assertEquals(TestConstants.WORKTYPE_COUNT, comicService.findAllWorktypes(null).size()); + } + + @Test + @Order(10) + void testFindComicByTitle() { + assertNull(comicService.findComicByTitle(null)); + assertNotNull(comicService.findComicByTitle("Danger Girl")); + assertNull(comicService.findComicByTitle("x-men")); + } + + @Test + @Order(11) + void testSaveComic() { + Publisher marvel = comicService.findAllPublishers("Marvel").get(0); + Comic comic = new Comic(); + comic.setTitle(TestConstants.COMIC_TITLE); + comic.setPublisher(marvel); + comicService.saveComic(comic); + assertEquals(TestConstants.COMIC_COUNT + 1, comicService.findAllComics(null).size()); + } + + @Test + @Order(12) + void testDeleteComic() { + assertEquals(TestConstants.COMIC_COUNT + 1, comicService.findAllComics(null).size()); + List comics = comicService.findAllComics(TestConstants.COMIC_TITLE); + assertEquals(1, comics.size()); + Comic comic = comicService.findComicByTitle(TestConstants.COMIC_TITLE); + assertNotNull(comic); + comicService.deleteComic(comic); + Publisher marvel = comicService.findPublisherByName("Marvel"); + assertNotNull(marvel.getComics()); + assertEquals(TestConstants.MARVEL_COMIC_COUNT, marvel.getComics().size()); + assertEquals(0, comicService.findAllComics(TestConstants.COMIC_TITLE).size()); + assertEquals(TestConstants.COMIC_COUNT, comicService.findAllComics(null).size()); + } + + @Test + @Order(13) + void testFindAllIssues() { + assertEquals(TestConstants.ISSUE_COUNT, comicService.findAllIssues().size()); + } + + @Test + @Order(14) + void testFindAllIssuesForComic() { + Comic comic = TestConstants.getComicWithIssues(comicService); + List issues = comicService.findAllIssuesForComic(comic); + assertEquals(12, issues.size()); + } + + @Test + @Order(15) + void testSaveIssue() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + Issue issue = new Issue(); + issue.setComic(comic); + issue.setIssueNumber(TestConstants.ISSUE_ISSUENUMBER); + comicService.saveIssue(issue); + assertEquals(TestConstants.ISSUE_COUNT + 1, comicService.findAllIssues().size()); + comic = TestConstants.getComicWithoutReferences(comicService); + assertNotNull(comic.getIssues()); + assertEquals(1, comic.getIssues().size()); + } + + @Test + @Order(16) + void testDeleteIssue() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + List issues = comicService.findAllIssuesForComic(comic); + assertEquals(1, issues.size()); + comicService.deleteIssue(issues.get(0)); + assertEquals(TestConstants.ISSUE_COUNT, comicService.findAllIssues().size()); + } + + @Test + @Order(17) + void testFindAllTradePaperbacks() { + assertEquals(TestConstants.TRADEPAPERBACK_COUNT, comicService.findAllTradePaperbacks(null).size()); + assertEquals(7, comicService.findAllTradePaperbacks("of").size()); + } + + @Test + @Order(18) + void testSaveTradePaperBack() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + TradePaperback tradePaperback = new TradePaperback(); + tradePaperback.setComic(comic); + tradePaperback.setName(TestConstants.TRADEPAPERBACK_NAME); + comicService.saveTradePaperBack(tradePaperback); + assertEquals(TestConstants.TRADEPAPERBACK_COUNT + 1, comicService.findAllTradePaperbacks(null).size()); + } + + @Test + @Order(19) + void testDeleteTradePaperBack() { + List tradePaperbacks = comicService.findAllTradePaperbacks(TestConstants.TRADEPAPERBACK_NAME); + assertEquals(1, tradePaperbacks.size()); + comicService.deleteTradePaperBack(tradePaperbacks.get(0)); + assertEquals(TestConstants.TRADEPAPERBACK_COUNT, comicService.findAllTradePaperbacks(null).size()); + } + + @Test + @Order(20) + void testFindAllStoryArcs() { + assertEquals(TestConstants.STORYARC_COUNT, comicService.findAllStoryArcs().size()); + } + + @Test + @Order(21) + void testFindAllStoryArcsForComic() { + assertEquals(TestConstants.STORYARC_COUNT, comicService.findAllStoryArcsForComic(null).size()); + Comic comic = TestConstants.getComicWithStoryArcs(comicService); + assertEquals(3, comicService.findAllStoryArcsForComic(comic).size()); + } + + @Test + @Order(22) + void testSaveStoryArc() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + StoryArc storyArc = new StoryArc(); + storyArc.setName(TestConstants.STORYARC_NAME); + storyArc.setComic(comic); + comicService.saveStoryArc(storyArc); + assertEquals(TestConstants.STORYARC_COUNT + 1, comicService.findAllStoryArcs().size()); + } + + @Test + @Order(23) + void testDeleteStoryArc() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + List storyArcs = comicService.findAllStoryArcsForComic(comic); + assertEquals(1, storyArcs.size()); + comicService.deleteStoryArc(storyArcs.get(0)); + assertEquals(TestConstants.STORYARC_COUNT, comicService.findAllStoryArcs().size()); + } + + @Test + @Order(24) + void testFindAllVolumes() { + assertEquals(TestConstants.VOLUME_COUNT, comicService.findAllVolumes().size()); + } + + @Test + @Order(25) + void testSaveVolume() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + Volume volume = new Volume(); + volume.setName(TestConstants.VOLUME_NAME); + volume.setComic(comic); + comicService.saveVolume(volume); + assertEquals(TestConstants.VOLUME_COUNT + 1, comicService.findAllVolumes().size()); + } + + @Test + @Order(26) + void testFindAllVolumesForComic() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + List volumes = comicService.findAllVolumesForComic(comic); + assertEquals(1, volumes.size()); + } + + @Test + @Order(27) + void testDeleteVolume() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + List volumes = comicService.findAllVolumesForComic(comic); + comicService.deleteVolume(volumes.get(0)); + assertEquals(TestConstants.VOLUME_COUNT, comicService.findAllVolumes().size()); + } + + @Test + @Order(28) + void testFindAllComicWorks() { + assertEquals(TestConstants.COMICWORK_COUNT, comicService.findAllComicWorks().size()); + } + + @Test + @Order(29) + void testSaveComicWork() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + Artist artist = TestConstants.getArtistWithoutReferences(comicService); + Worktype worktype = TestConstants.getWorktypeWithoutReferences(comicService); + ComicWork comicWork = new ComicWork(); + comicWork.setComic(comic); + comicWork.setArtist(artist); + comicWork.setWorkType(worktype); + comicService.saveComicWork(comicWork); + assertEquals(TestConstants.COMICWORK_COUNT + 1, comicService.findAllComicWorks().size()); + } + + @Test + @Order(30) + void testDeleteComicWork() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + Artist artist = TestConstants.getArtistWithoutReferences(comicService); + Worktype worktype = TestConstants.getWorktypeWithoutReferences(comicService); + List comicWorks = comic.getComicWorks(); + assertNotNull(comicWorks); + assertNotNull(artist.getComicWorks()); + assertNotNull(worktype.getComicWorks()); + assertEquals(1, comicWorks.size()); + assertEquals(1, artist.getComicWorks().size()); + assertEquals(1, worktype.getComicWorks().size()); + ComicWork comicWork = comicWorks.get(0); + comicService.deleteComicWork(comicWork); + assertEquals(TestConstants.COMICWORK_COUNT, comicService.findAllComicWorks().size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/media/TestConstants.java b/springboot/src/test/java/de/thpeetz/kontor/media/TestConstants.java new file mode 100644 index 0000000..32ee883 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/media/TestConstants.java @@ -0,0 +1,5 @@ +package de.thpeetz.kontor.media; + +public class TestConstants { + public static final String URL = "https://example.com/link.mp4"; +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/media/data/MediaArticleTest.java b/springboot/src/test/java/de/thpeetz/kontor/media/data/MediaArticleTest.java new file mode 100644 index 0000000..2f2adc7 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/media/data/MediaArticleTest.java @@ -0,0 +1,32 @@ +package de.thpeetz.kontor.media.data; + +import de.thpeetz.kontor.media.services.MediaArticleService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@SpringBootTest +public class MediaArticleTest { + + @Autowired + private MediaArticleRepository mediaArticleRepository; + + @Test + void checkInitialLoad() { + assertTrue(mediaArticleRepository.findAll().isEmpty()); + } + + @Test + void checkDefaultValues() { + MediaArticle mediaArticle = new MediaArticle(); + assertNull(mediaArticle.getUrl()); + assertNull(mediaArticle.getTitle()); + assertNull(mediaArticle.getCreatedDate()); + assertNull(mediaArticle.getLastModifiedDate()); + assertNull(mediaArticle.getId()); + assertFalse(mediaArticle.isReview()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/media/data/MediaFileTest.java b/springboot/src/test/java/de/thpeetz/kontor/media/data/MediaFileTest.java new file mode 100644 index 0000000..6ca61ce --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/media/data/MediaFileTest.java @@ -0,0 +1,37 @@ +package de.thpeetz.kontor.media.data; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +public class MediaFileTest { + + @Autowired + private MediaFileRepository mediaFileRepository; + + @Test + void checkInitialDataLoad() { + List mediaFileList = mediaFileRepository.findAll(); + assertTrue(mediaFileList.isEmpty()); + } + + @Test + void checkDefaultValues() { + MediaFile mediaFile = new MediaFile(); + assertNull(mediaFile.getUrl()); + assertNull(mediaFile.getTitle()); + assertNull(mediaFile.getFileName()); + assertNull(mediaFile.getPath()); + assertNull(mediaFile.getCloudLink()); + assertNull(mediaFile.getCreatedDate()); + assertNull(mediaFile.getLastModifiedDate()); + assertNull(mediaFile.getId()); + assertFalse(mediaFile.isReview()); + assertFalse(mediaFile.isShouldDownload()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/media/data/MediaVideoTest.java b/springboot/src/test/java/de/thpeetz/kontor/media/data/MediaVideoTest.java new file mode 100644 index 0000000..1a40335 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/media/data/MediaVideoTest.java @@ -0,0 +1,38 @@ +package de.thpeetz.kontor.media.data; + +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@SpringBootTest +public class MediaVideoTest { + + @Autowired + private MediaVideoRepository mediaVideoRepository; + + @Test + @Order(1) + void checkInitialLoad() { + assertTrue(mediaVideoRepository.findAll().isEmpty()); + } + + @Test + @Order(2) + void checkDefaultValues() { + MediaVideo mediaVideo = new MediaVideo(); + assertNull(mediaVideo.getUrl()); + assertNull(mediaVideo.getTitle()); + assertNull(mediaVideo.getFileName()); + assertNull(mediaVideo.getPath()); + assertNull(mediaVideo.getCloudLink()); + assertNull(mediaVideo.getCreatedDate()); + assertNull(mediaVideo.getLastModifiedDate()); + assertNull(mediaVideo.getId()); + assertFalse(mediaVideo.isReview()); + assertFalse(mediaVideo.isShouldDownload()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/media/services/MediaArticleServiceTest.java b/springboot/src/test/java/de/thpeetz/kontor/media/services/MediaArticleServiceTest.java new file mode 100644 index 0000000..6178aa8 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/media/services/MediaArticleServiceTest.java @@ -0,0 +1,50 @@ +package de.thpeetz.kontor.media.services; + +import de.thpeetz.kontor.media.TestConstants; +import de.thpeetz.kontor.media.data.MediaArticle; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@Slf4j +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MediaArticleServiceTest { + + @Autowired + MediaArticleService mediaArticleService; + + @Test + @Order(1) + void findAllMediaArticles() { + assertTrue(mediaArticleService.findAllMediaArticles(null).isEmpty()); + } + + @Test + @Order(2) + void saveMediaArticle() { + int mediaArticleCount = mediaArticleService.findAllMediaArticles(null).size(); + MediaArticle mediaArticle = new MediaArticle(); + mediaArticle.setUrl(TestConstants.URL); + mediaArticleService.saveMediaArticle(mediaArticle); + assertEquals(++mediaArticleCount, mediaArticleService.findAllMediaArticles(null).size()); + } + + @Test + @Order(3) + void deleteMediaArticle() { + int mediaArticleCount = mediaArticleService.findAllMediaArticles(null).size(); + List mediaArticleList = mediaArticleService.findAllMediaArticles(TestConstants.URL); + assertEquals(1, mediaArticleService.findAllMediaArticles(null).size()); + mediaArticleService.deleteMediaArticle(mediaArticleList.get(0)); + assertEquals(--mediaArticleCount, mediaArticleService.findAllMediaArticles(null).size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/media/services/MediaFileServiceTest.java b/springboot/src/test/java/de/thpeetz/kontor/media/services/MediaFileServiceTest.java new file mode 100644 index 0000000..2201be5 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/media/services/MediaFileServiceTest.java @@ -0,0 +1,48 @@ +package de.thpeetz.kontor.media.services; + +import de.thpeetz.kontor.media.TestConstants; +import de.thpeetz.kontor.media.data.MediaFile; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class MediaFileServiceTest { + + @Autowired + private MediaFileService mediaFileService; + + @Test + @Order(1) + void testFindAllMediaFiles() { + assertTrue(mediaFileService.findAllMediaFiles(null).isEmpty()); + } + + @Test + @Order(2) + void testSaveMediaFile() { + int mediaFileCount = mediaFileService.findAllMediaFiles(null).size(); + MediaFile mediaFile = new MediaFile(); + mediaFile.setUrl(TestConstants.URL); + mediaFileService.saveMediaFile(mediaFile); + assertEquals(mediaFileCount +1, mediaFileService.findAllMediaFiles(null).size()); + } + + @Test + @Order(3) + void testDeleteMediaFile() { + int mediaFileCount = mediaFileService.findAllMediaFiles(null).size(); + List mediaFileList = mediaFileService.findAllMediaFiles(TestConstants.URL); + assertEquals(1, mediaFileList.size()); + mediaFileService.deleteMediaFile(mediaFileList.get(0)); + assertEquals(--mediaFileCount, mediaFileService.findAllMediaFiles(null).size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/media/services/MediaVideoServiceTest.java b/springboot/src/test/java/de/thpeetz/kontor/media/services/MediaVideoServiceTest.java new file mode 100644 index 0000000..36a6fb1 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/media/services/MediaVideoServiceTest.java @@ -0,0 +1,48 @@ +package de.thpeetz.kontor.media.services; + +import de.thpeetz.kontor.media.TestConstants; +import de.thpeetz.kontor.media.data.MediaArticle; +import de.thpeetz.kontor.media.data.MediaVideo; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Slf4j +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class MediaVideoServiceTest { + + @Autowired + private MediaVideoService mediaVideoService; + + @Test + void findAllMediaVideos() { + assertTrue(mediaVideoService.findAllMediaVideos(null).isEmpty()); + } + + @Test + void saveMediaVideo() { + int mediaVideoCount = mediaVideoService.findAllMediaVideos(null).size(); + MediaVideo mediaVideo = new MediaVideo(); + mediaVideo.setUrl(TestConstants.URL); + mediaVideoService.saveMediaVideo(mediaVideo); + assertEquals(++mediaVideoCount, mediaVideoService.findAllMediaVideos(null).size()); + } + + @Test + void deleteMediaVideo() { + int mediaVideoCount = mediaVideoService.findAllMediaVideos(null).size(); + List mediaVideoList = mediaVideoService.findAllMediaVideos(TestConstants.URL); + assertEquals(1, mediaVideoList.size()); + mediaVideoService.deleteMediaVideo(mediaVideoList.get(0)); + assertEquals(--mediaVideoCount, mediaVideoService.findAllMediaVideos(null).size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/TestConstants.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/TestConstants.java new file mode 100644 index 0000000..a05c1dd --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/TestConstants.java @@ -0,0 +1,39 @@ +package de.thpeetz.kontor.tysc; + +public class TestConstants { + + public static final Integer CARD_COUNT = 10; + public static final Integer CARD_CARDNUMBER = 999; + public static final Integer CARD_YEAR = 2000; + public static final Integer CARDSET_COUNT = 15; + public static final Integer FOOTBALL_TEAM_COUNT = 33; + public static final Integer FOOTBALL_POSITION_COUNT = 25; + public static final Integer PLAYER_COUNT = 38; + public static final Integer PLAYER_CHRIS_COUNT = 3; + public static final Integer POSITION_COUNT = 44; + public static final Integer POSITION_CENTER_COUNT = 3; + public static final Integer ROOSTER_COUNT = 11; + public static final Integer ROOSTER_YEAR = 1900; + public static final Integer SPORT_COUNT = 4; + public static final Integer TEAM_COUNT = 122; + public static final Integer VENDOR_COUNT = 9; + public static final String CARDSET_NAME = "CardSet"; + public static final String CARDSET_MYSTIQUE_NAME = "Mystique"; + public static final String DOLPHINS_NAME = "Miami Dolphins"; + public static final Object DOLPHINS_SHORT = "Dolphins"; + public static final String FOOTBALL_NAME = "Football"; + public static final String PLAYER_FIRSTNAME = "FirstName"; + public static final String PLAYER_LASTNAME = "LastName"; + public static final String PLAYER_CHRIS_NAME = "Chris"; + public static final String POSITION_CENTER = "Center"; + public static final String POSITION_NAME = "Position"; + public static final String POSITION_SHORTNAME = "POS"; + public static final String QUARTERBACK_NAME = "Quarterback"; + public static final String QUARTERBACK_SHORTNAME = "QB"; + public static final String SPORT_NAME = "Sport"; + public static final String TEAM_NAME = "Musterstadt Team"; + public static final String TEAM_SHORTNAME = "Team"; + public static final String TOPPS_NAME = "Topps"; + public static final String VENDOR_FLEER = "Fleer"; + public static final String VENDOR_NAME = "Vendor"; +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardRepositoryTest.java new file mode 100644 index 0000000..e2ff8b2 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardRepositoryTest.java @@ -0,0 +1,35 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CardRepositoryTest { + + @Autowired + private CardRepository cardRepository; + + @Test + void testSearchByFields() { + List cards = cardRepository.findAll(); + Card existingCard = cards.get(3); + Vendor vendor = existingCard.getVendor(); + CardSet cardSet = existingCard.getCardSet(); + Rooster rooster = existingCard.getRooster(); + int cardNumber = existingCard.getCardNumber(); + int year = existingCard.getYear(); + Card card = cardRepository.search(vendor, cardSet, rooster, cardNumber, year); + assertNotNull(card); + } + + @Test + void testSearch() { + assertEquals(2, cardRepository.search("2").size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardSetRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardSetRepositoryTest.java new file mode 100644 index 0000000..71a08ba --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardSetRepositoryTest.java @@ -0,0 +1,42 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class CardSetRepositoryTest { + + @Autowired + private CardSetRepository cardSetRepository; + + @Autowired + private VendorRepository vendorRepository; + + @Test + void testFindByName() { + List cardSets = cardSetRepository.findByName(TestConstants.CARDSET_MYSTIQUE_NAME); + assertEquals(1, cardSets.size()); + } + + @Test + void testFindByNameAndVendor() { + Vendor fleer = vendorRepository.findByName(TestConstants.VENDOR_FLEER); + assertNotNull(fleer); + CardSet cardSet = cardSetRepository.findByNameAndVendor(TestConstants.CARDSET_MYSTIQUE_NAME, fleer); + assertNotNull(cardSet); + } + + @Test + void testSearch() { + List cardSets = cardSetRepository.search("SP"); + assertEquals(3, cardSets.size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardSetTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardSetTest.java new file mode 100644 index 0000000..a5699d6 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardSetTest.java @@ -0,0 +1,22 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; +import de.thpeetz.kontor.tysc.services.CardService; + +@SpringBootTest +class CardSetTest { + + @Autowired + private CardService cardService; + + @Test + void checkInitialDataLoad() { + assertEquals(TestConstants.CARDSET_COUNT, cardService.findAllCardSets(null).size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardTest.java new file mode 100644 index 0000000..0d71d8d --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/CardTest.java @@ -0,0 +1,22 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; +import de.thpeetz.kontor.tysc.services.CardService; + +@SpringBootTest +class CardTest { + + @Autowired + private CardService cardService; + + @Test + void checkInitialDataLoad() { + assertEquals(TestConstants.CARD_COUNT, cardService.findAllCards(null).size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/FieldPositionRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/FieldPositionRepositoryTest.java new file mode 100644 index 0000000..a9273db --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/FieldPositionRepositoryTest.java @@ -0,0 +1,59 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class FieldPositionRepositoryTest { + + @Autowired + private FieldPositionRepository fieldPositionRepository; + + @Autowired + private SportRepository sportRepository; + + @Test + void testFindByShortName() { + FieldPosition position = fieldPositionRepository.findByShortName(TestConstants.QUARTERBACK_SHORTNAME); + assertNotNull(position); + assertEquals(TestConstants.QUARTERBACK_NAME, position.getName()); + } + + @Test + void testFindByShortNameAndSport() { + Sport sport = sportRepository.findByName(TestConstants.FOOTBALL_NAME); + FieldPosition position = fieldPositionRepository.findByShortNameAndSport(TestConstants.QUARTERBACK_SHORTNAME, sport); + assertNotNull(position); + assertEquals(TestConstants.QUARTERBACK_NAME, position.getName()); + } + + @Test + void testFindByShortNameIgnoreCase() { + List positions = fieldPositionRepository + .findByShortNameIgnoreCase(TestConstants.QUARTERBACK_SHORTNAME.toLowerCase()); + assertNotNull(positions); + assertEquals(1, positions.size()); + assertEquals(TestConstants.QUARTERBACK_NAME, positions.get(0).getName()); + } + + @Test + void testFindBySport() { + Sport sport = sportRepository.findByName(TestConstants.FOOTBALL_NAME); + List positions = fieldPositionRepository.findBySport(sport); + assertEquals(TestConstants.FOOTBALL_POSITION_COUNT, positions.size()); + } + + @Test + void testSearch() { + List positions = fieldPositionRepository.search("back"); + assertEquals(8, positions.size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/FieldPositionTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/FieldPositionTest.java new file mode 100644 index 0000000..8a8b9b7 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/FieldPositionTest.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class FieldPositionTest { + + @Autowired + private FieldPositionRepository fieldPositionRepository; + + @Test + void checkInitialDataLoad() { + assertEquals(Integer.toUnsignedLong(TestConstants.POSITION_COUNT), fieldPositionRepository.count()); + } + + @Test + void throwExceptionWhenPositionSavedWithEmptyName() { + FieldPosition fieldPosition = new FieldPosition(); + assertThrows(TransactionSystemException.class, () -> { + fieldPositionRepository.save(fieldPosition); + }); + fieldPosition.setName("Position"); + assertThrows(TransactionSystemException.class, () -> { + fieldPositionRepository.save(fieldPosition); + }); + } + + @Test + void throwExceptionWhenPositionSavedWithExistingName() { + FieldPosition existingFieldPosition = fieldPositionRepository.findAll().get(11); + FieldPosition fieldPosition = new FieldPosition(); + fieldPosition.setName(existingFieldPosition.getName()); + assertThrows(TransactionSystemException.class, () -> { + fieldPositionRepository.save(fieldPosition); + }); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/PlayerRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/PlayerRepositoryTest.java new file mode 100644 index 0000000..7cbd540 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/PlayerRepositoryTest.java @@ -0,0 +1,31 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class PlayerRepositoryTest { + + @Autowired + private PlayerRepository playerRepository; + + @Test + void testFindByFirstNameAndLastName() { + Player existingPlayer = playerRepository.findAll().get(11); + String firstName = existingPlayer.getFirstName(); + String lastName = existingPlayer.getLastName(); + Player found = playerRepository.findByFirstNameAndLastName(firstName, lastName); + assertEquals(existingPlayer.getFullName(), found.getFullName()); + } + + @Test + void testSearch() { + List players = playerRepository.search("can"); + assertEquals(1, players.size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/PlayerTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/PlayerTest.java new file mode 100644 index 0000000..941537b --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/PlayerTest.java @@ -0,0 +1,53 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class PlayerTest { + + @Autowired + private PlayerRepository playerRepository; + + @Test + @Order(1) + void checkInitialDataLoad() { + List players = playerRepository.findAll(); + assertEquals(TestConstants.PLAYER_COUNT, players.size()); + } + + @Test + @Order(2) + void throwExceptionWhenPlayerSavedWithEmptyName() { + Player player = new Player(); + assertThrows(TransactionSystemException.class, () -> { + playerRepository.save(player); + }); + } + + @Test + @Order(3) + void throwExceptionWhenPlayerSavedWithExistingName() { + Player existingPlayer = playerRepository.findAll().get(10); + assertNotNull(existingPlayer); + Player player = new Player(); + player.setFirstName(existingPlayer.getFirstName()); + player.setLastName(existingPlayer.getLastName()); + assertThrows(DataIntegrityViolationException.class, () -> { + playerRepository.save(player); + }); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/RoosterRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/RoosterRepositoryTest.java new file mode 100644 index 0000000..fa56d90 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/RoosterRepositoryTest.java @@ -0,0 +1,29 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class RoosterRepositoryTest { + + @Autowired + private RoosterRepository roosterRepository; + + @Test + void testFindByReferences() { + List roosters = roosterRepository.findAll(); + Rooster existingRooster = roosters.get(4); + Team team = existingRooster.getTeam(); + Player player = existingRooster.getPlayer(); + FieldPosition position = existingRooster.getPosition(); + int year = existingRooster.getYear(); + Rooster rooster = roosterRepository.findByReferences(player, team, position, year); + assertNotNull(rooster); + assertEquals(existingRooster.getId(), rooster.getId()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/RoosterTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/RoosterTest.java new file mode 100644 index 0000000..caffea4 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/RoosterTest.java @@ -0,0 +1,47 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class RoosterTest { + + @Autowired + private RoosterRepository roosterRepository; + + @Test + void checkInitialDataLoad() { + List roosters = roosterRepository.findAll(); + assertEquals(TestConstants.ROOSTER_COUNT, roosters.size()); + } + + @Test + void checkFetchingData() { + Rooster rooster = roosterRepository.findAll().get(4); + assertNotNull(rooster.getTeam()); + assertNotNull(rooster.getPlayer()); + assertNotNull(rooster.getPosition()); + } + + @Test + void exceptionThrownWhenSavingDuplicateRooster() { + List roosters = roosterRepository.findAll(); + Rooster existingRooster = roosters.get(5); + Rooster rooster = new Rooster(); + rooster.setPlayer(existingRooster.getPlayer()); + rooster.setTeam(existingRooster.getTeam()); + rooster.setPosition(existingRooster.getPosition()); + rooster.setYear(existingRooster.getYear()); + assertThrows(DataIntegrityViolationException.class, () -> { + roosterRepository.save(rooster); + }); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/SportRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/SportRepositoryTest.java new file mode 100644 index 0000000..c0db4be --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/SportRepositoryTest.java @@ -0,0 +1,40 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class SportRepositoryTest { + + @Autowired + private SportRepository sportRepository; + + @Test + void testFindByName() { + Sport sport = sportRepository.findByName(TestConstants.FOOTBALL_NAME); + assertNotNull(sport); + assertEquals(TestConstants.FOOTBALL_TEAM_COUNT, sport.getTeams().size()); + assertEquals(TestConstants.FOOTBALL_POSITION_COUNT, sport.getPositions().size()); + } + + @Test + void testFindByNameIgnoreCase() { + List sports = sportRepository.findByNameIgnoreCase(TestConstants.FOOTBALL_NAME.toLowerCase()); + assertNotNull(sports); + assertEquals(TestConstants.FOOTBALL_TEAM_COUNT, sports.get(0).getTeams().size()); + } + + @Test + void testSearch() { + List sports = sportRepository.search("ball"); + assertEquals(3, sports.size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/SportTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/SportTest.java new file mode 100644 index 0000000..e47bb50 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/SportTest.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class SportTest { + + @Autowired + private SportRepository sportRepository; + + @Test + void checkInitialDataLoad() { + List sports = sportRepository.findAll(); + assertEquals(TestConstants.SPORT_COUNT, sports.size()); + } + + @Test + void exceptionThrownWhenSavingSportWithEmptyName() { + Sport sport = new Sport(); + assertThrows(TransactionSystemException.class, () -> { + sportRepository.save(sport); + }); + } + + @Test + void exceptionThrownWhenSavingSportWithExistingName() { + Sport existingSport = sportRepository.findAll().get(0); + Sport sport = new Sport(); + sport.setName(existingSport.getName()); + assertThrows(DataIntegrityViolationException.class, () -> { + sportRepository.save(sport); + }); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/TeamRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/TeamRepositoryTest.java new file mode 100644 index 0000000..86f4200 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/TeamRepositoryTest.java @@ -0,0 +1,37 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class TeamRepositoryTest { + + @Autowired + private TeamRepository teamRepository; + + @Test + void testFindByName() { + Team team = teamRepository.findByName(TestConstants.DOLPHINS_NAME); + assertEquals(TestConstants.DOLPHINS_SHORT, team.getShortName()); + } + + @Test + void testFindByNameIgnoreCase() { + List teams = teamRepository.findByNameIgnoreCase(TestConstants.DOLPHINS_NAME.toLowerCase()); + assertEquals(1, teams.size()); + assertEquals(TestConstants.DOLPHINS_SHORT, teams.get(0).getShortName()); + } + + @Test + void testSearch() { + List teams = teamRepository.search("dol"); + assertEquals(1, teams.size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/TeamTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/TeamTest.java new file mode 100644 index 0000000..7bd4ba2 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/TeamTest.java @@ -0,0 +1,53 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class TeamTest { + + @Autowired + private TeamRepository teamRepository; + + @Test + void checkInitialDataLoad() { + List teams = teamRepository.findAll(); + assertEquals(TestConstants.TEAM_COUNT, teams.size()); + } + + @Test + void throwExceptionWhenTeamSavedWithEmptyName() { + Team team = new Team(); + assertThrows(TransactionSystemException.class, () -> { + teamRepository.save(team); + }); + } + + @Test + void throwExceptionWhenTeamSavedWithoutSport() { + Team team = new Team(); + team.setName(TestConstants.TEAM_NAME); + assertThrows(TransactionSystemException.class, () -> { + teamRepository.save(team); + }); + } + + @Test + void throwExceptionWhenTeamSavedWithExistingName() { + Team existingTeam = teamRepository.findAll().get(11); + Team team = new Team(); + team.setName(existingTeam.getName()); + team.setSport(existingTeam.getSport()); + assertThrows(TransactionSystemException.class, () -> { + teamRepository.save(team); + }); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/VendorRepositoryTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/VendorRepositoryTest.java new file mode 100644 index 0000000..9db7ef7 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/VendorRepositoryTest.java @@ -0,0 +1,41 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class VendorRepositoryTest { + + @Autowired + private VendorRepository vendorRepository; + + @Test + void testFindByName() { + Vendor vendor = vendorRepository.findByName(TestConstants.TOPPS_NAME); + assertNotNull(vendor); + assertEquals(TestConstants.TOPPS_NAME, vendor.getName()); + } + + @Test + void testFindByNameIgnoreCase() { + List vendors = vendorRepository.findByNameIgnoreCase(TestConstants.TOPPS_NAME.toLowerCase()); + assertNotNull(vendors); + assertEquals(1, vendors.size()); + assertEquals(TestConstants.TOPPS_NAME, vendors.get(0).getName()); + } + + @Test + void testSearch() { + List vendors = vendorRepository.search("pp"); + assertNotNull(vendors); + assertEquals(2, vendors.size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/data/VendorTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/VendorTest.java new file mode 100644 index 0000000..a16bd77 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/data/VendorTest.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class VendorTest { + + @Autowired + private VendorRepository vendorRepository; + + @Test + void checkInitialDataLoad() { + List vendors = vendorRepository.findAll(); + assertEquals(TestConstants.VENDOR_COUNT, vendors.size()); + } + + @Test + void exceptionThrownWhenSavingVendortWithEmptyName() { + Vendor vendor = new Vendor(); + assertThrows(TransactionSystemException.class, () -> { + vendorRepository.save(vendor); + }); + } + + @Test + void exceptionThrownWhenSavingSportWithExistingName() { + Vendor vendor = new Vendor(); + vendor.setName(TestConstants.TOPPS_NAME); + assertThrows(DataIntegrityViolationException.class, () -> { + vendorRepository.save(vendor); + }); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/services/CardServiceTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/services/CardServiceTest.java new file mode 100644 index 0000000..e215217 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/services/CardServiceTest.java @@ -0,0 +1,126 @@ +package de.thpeetz.kontor.tysc.services; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; +import de.thpeetz.kontor.tysc.data.Card; +import de.thpeetz.kontor.tysc.data.CardSet; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.Vendor; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class CardServiceTest { + + @Autowired + private CardService cardService; + + @Autowired + private SportService sportService; + + @Test + @Order(1) + void testFindAllVendors() { + assertEquals(TestConstants.VENDOR_COUNT, cardService.findAllVendors(null).size()); + assertEquals(2, cardService.findAllVendors("pp").size()); + } + + @Test + @Order(2) + void testSaveVendor() { + Vendor vendor = new Vendor(); + vendor.setName(TestConstants.VENDOR_NAME); + vendor = cardService.saveVendor(vendor); + assertNotNull(vendor); + assertEquals(TestConstants.VENDOR_COUNT + 1, cardService.findAllVendors(null).size()); + } + + @Test + @Order(3) + void testDeleteVendor() { + List vendors = cardService.findAllVendors(TestConstants.VENDOR_NAME); + assertEquals(1, vendors.size()); + Vendor vendor = vendors.get(0); + cardService.deleteVendor(vendor); + assertEquals(TestConstants.VENDOR_COUNT, cardService.findAllVendors(null).size()); + } + + @Test + @Order(4) + void testFindAllCardSets() { + assertEquals(TestConstants.CARDSET_COUNT, cardService.findAllCardSets(null).size()); + assertEquals(1, cardService.findAllCardSets("Ultra").size()); + } + + @Test + @Order(5) + void testSaveCardSet() { + List vendors = cardService.findAllVendors(TestConstants.VENDOR_FLEER); + assertEquals(1, vendors.size()); + Vendor vendor = vendors.get(0); + CardSet cardSet = new CardSet(); + cardSet.setName(TestConstants.CARDSET_NAME); + cardSet.setVendor(vendor); + cardSet.setInsertSet(false); + cardSet.setParallelSet(false); + cardService.saveCardSet(cardSet); + assertEquals(TestConstants.CARDSET_COUNT + 1, cardService.findAllCardSets(null).size()); + } + + @Test + @Order(6) + void testDeleteCardSet() { + List cardSets = cardService.findAllCardSets(TestConstants.CARDSET_NAME); + assertEquals(1, cardSets.size()); + CardSet cardSet = cardSets.get(0); + cardService.deleteCardSet(cardSet); + assertEquals(TestConstants.CARDSET_COUNT, cardService.findAllCardSets(null).size()); + } + + @Test + @Order(7) + void testFindAllCards() { + assertEquals(TestConstants.CARD_COUNT, cardService.findAllCards(null).size()); + assertEquals(1, cardService.findAllCards("112").size()); + } + + @Test + @Order(8) + void testSaveCard() { + List vendors = cardService.findAllVendors(TestConstants.VENDOR_FLEER); + assertEquals(1, vendors.size()); + Vendor vendor = vendors.get(0); + List cardSets = cardService.findAllCardSets(TestConstants.CARDSET_MYSTIQUE_NAME); + assertEquals(3, cardSets.size()); + CardSet cardSet = cardSets.get(0); + List roosters = sportService.findAllRoosters(); + Rooster rooster = roosters.get(0); + Card card = new Card(); + card.setCardNumber(TestConstants.CARD_CARDNUMBER); + card.setCardSet(cardSet); + card.setVendor(vendor); + card.setRooster(rooster); + card.setYear(TestConstants.CARD_YEAR); + cardService.saveCard(card); + assertEquals(TestConstants.CARD_COUNT + 1, cardService.findAllCards(null).size()); + } + + @Test + @Order(9) + void testDeleteCard() { + List cards = cardService.findAllCards(TestConstants.CARD_CARDNUMBER.toString()); + assertEquals(1, cards.size()); + Card card = cards.get(0); + cardService.deleteCard(card); + assertEquals(TestConstants.CARD_COUNT, cardService.findAllCards(null).size()); + } +} diff --git a/springboot/src/test/java/de/thpeetz/kontor/tysc/services/SportServiceTest.java b/springboot/src/test/java/de/thpeetz/kontor/tysc/services/SportServiceTest.java new file mode 100644 index 0000000..30b7ed6 --- /dev/null +++ b/springboot/src/test/java/de/thpeetz/kontor/tysc/services/SportServiceTest.java @@ -0,0 +1,200 @@ +package de.thpeetz.kontor.tysc.services; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import de.thpeetz.kontor.tysc.data.FieldPosition; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; +import de.thpeetz.kontor.tysc.data.Player; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.Sport; +import de.thpeetz.kontor.tysc.data.Team; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class SportServiceTest { + + @Autowired + private SportService sportService; + + + @Test + @Order(1) + void testFindAllSports() { + List sports = sportService.findAllSports(null); + assertEquals(TestConstants.SPORT_COUNT, sports.size()); + assertEquals(3, sportService.findAllSports("ball").size()); + } + + @Test + @Order(2) + void testSaveSport() { + Sport sport = new Sport(); + sport.setName(TestConstants.SPORT_NAME); + sport = sportService.saveSport(sport); + assertNotNull(sport); + assertEquals(TestConstants.SPORT_COUNT+1, sportService.findAllSports(null).size()); + } + + @Test + @Order(3) + void testDeleteSport() { + List sports = sportService.findAllSports(TestConstants.SPORT_NAME); + assertEquals(1, sports.size()); + Sport sport = sports.get(0); + sportService.deleteSport(sport); + assertEquals(TestConstants.SPORT_COUNT, sportService.findAllSports(null).size()); + } + + @Test + @Order(4) + void testFindAllPlayers() { + assertEquals(TestConstants.PLAYER_COUNT, sportService.findAllPlayers(null).size()); + assertEquals(TestConstants.PLAYER_CHRIS_COUNT, sportService.findAllPlayers(TestConstants.PLAYER_CHRIS_NAME).size()); + } + + @Test + @Order(5) + void testSavePlayer() { + Player player = new Player(); + player.setFirstName(TestConstants.PLAYER_FIRSTNAME); + player.setLastName(TestConstants.PLAYER_LASTNAME); + player = sportService.savePlayer(player); + assertNotNull(player); + } + + @Test + @Order(6) + void testDeletePlayer() { + List players = sportService.findAllPlayers(TestConstants.PLAYER_LASTNAME); + assertEquals(1, players.size()); + Player player = players.get(0); + sportService.deletePlayer(player); + assertEquals(TestConstants.PLAYER_COUNT, sportService.findAllPlayers(null).size()); + } + + + @Test + @Order(7) + void testFindAllPositions() { + assertEquals(TestConstants.POSITION_COUNT, sportService.findAllPositions(null).size()); + assertEquals(TestConstants.POSITION_CENTER_COUNT, sportService.findAllPositions(TestConstants.POSITION_CENTER).size()); + } + + @Test + @Order(8) + void testFindAllPositionsForSport() { + List sports = sportService.findAllSports(TestConstants.FOOTBALL_NAME); + assertEquals(1, sports.size()); + Sport football = sports.get(0); + assertEquals(TestConstants.FOOTBALL_POSITION_COUNT, sportService.findAllPositionsForSport(football).size()); + } + + @Test + @Order(9) + void testSavePosition() { + List sports = sportService.findAllSports(TestConstants.FOOTBALL_NAME); + Sport football = sports.get(0); + FieldPosition position = new FieldPosition(); + position.setSport(football); + position.setName(TestConstants.POSITION_NAME); + position.setShortName(TestConstants.POSITION_SHORTNAME); + sportService.savePosition(position); + assertEquals(TestConstants.POSITION_COUNT+1, sportService.findAllPositions(null).size()); + } + + @Test + @Order(10) + void testDeletePosition() { + List positions = sportService.findAllPositions(TestConstants.POSITION_NAME); + assertEquals(1, positions.size()); + FieldPosition position = positions.get(0); + sportService.deletePosition(position); + assertEquals(TestConstants.POSITION_COUNT, sportService.findAllPositions(null).size()); + } + + @Test + @Order(11) + void testFindAllTeams() { + assertEquals(TestConstants.TEAM_COUNT, sportService.findAllTeams(null).size()); + assertEquals(1, sportService.findAllTeams("sharks").size()); + } + + @Test + @Order(12) + void testSaveTeam() { + List sports = sportService.findAllSports(TestConstants.FOOTBALL_NAME); + Sport football = sports.get(0); + Team team = new Team(); + team.setSport(football); + team.setName(TestConstants.TEAM_NAME); + team.setShortName(TestConstants.TEAM_SHORTNAME); + sportService.saveTeam(team); + assertEquals(TestConstants.TEAM_COUNT+1, sportService.findAllTeams(null).size()); + } + + @Test + @Order(13) + void testDeleteTeam() { + List teams = sportService.findAllTeams(TestConstants.TEAM_NAME); + assertEquals(1, teams.size()); + Team team = teams.get(0); + sportService.deleteTeam(team); + assertEquals(TestConstants.TEAM_COUNT, sportService.findAllTeams(null).size()); + } + + @Test + @Order(14) + void testFindAllRoosters() { + assertEquals(TestConstants.ROOSTER_COUNT, sportService.findAllRoosters().size()); + } + + @Test + @Order(15) + void testFindRoosterByFields() { + List roosters = sportService.findAllRoosters(); + Rooster existingRooster = roosters.get(4); + Team team = existingRooster.getTeam(); + Player player = existingRooster.getPlayer(); + FieldPosition position = existingRooster.getPosition(); + int year = existingRooster.getYear(); + Rooster rooster = sportService.findRoosterByFields(team, player, position, year); + assertNotNull(rooster); + assertEquals(existingRooster.getId(), rooster.getId()); + } + + @Test + @Order(16) + void testSaveRooster() { + Rooster rooster = new Rooster(); + Player player = sportService.findAllPlayers(TestConstants.PLAYER_CHRIS_NAME).get(0); + rooster.setPlayer(player); + Team team = sportService.findAllTeams(TestConstants.DOLPHINS_NAME).get(0); + rooster.setTeam(team); + FieldPosition position = sportService.findAllPositions(TestConstants.QUARTERBACK_NAME).get(0); + rooster.setPosition(position); + rooster.setYear(TestConstants.ROOSTER_YEAR); + sportService.saveRooster(rooster); + assertEquals(TestConstants.ROOSTER_COUNT+1, sportService.findAllRoosters().size()); + } + + @Test + @Order(17) + void testDeleteRooster() { + Team team = sportService.findAllTeams(TestConstants.DOLPHINS_NAME).get(0); + Player player = sportService.findAllPlayers(TestConstants.PLAYER_CHRIS_NAME).get(0); + FieldPosition position = sportService.findAllPositions(TestConstants.QUARTERBACK_NAME).get(0); + Rooster rooster = sportService.findRoosterByFields(team, player, position, TestConstants.ROOSTER_YEAR); + assertNotNull(rooster); + sportService.deleteRooster(rooster); + assertEquals(TestConstants.ROOSTER_COUNT, sportService.findAllRoosters().size()); + } +} diff --git a/springboot/src/test/resources/application.properties b/springboot/src/test/resources/application.properties new file mode 100644 index 0000000..53be2f4 --- /dev/null +++ b/springboot/src/test/resources/application.properties @@ -0,0 +1,30 @@ +server.port=8085 + +spring.hibernate.dialect=org.hibernate.dialect.HSQLDialect +spring.jpa.database-platform=org.hibernate.dialect.HSQLDialect +spring.datasource.driverClassName=org.hsqldb.jdbc.JDBCDriver +spring.datasource.url=jdbc:hsqldb:mem:testDb +spring.datasource.username=sa +spring.datasource.password=sa + +#spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect +#spring.datasource.driverClassName=org.sqlite.JDBC +#spring.datasource.url=jdbc:sqlite:file:./kontorTesrDb?cache=shared +#spring.datasource.username=sa +#spring.datasource.password=sa + +spring.jpa.defer-datasource-initialization = true +#spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=false +spring.sql.init.mode=always + +spring.mustache.check-template-location = false + +logging.level.org.atmosphere=INFO +logging.level.org.springframework.web=INFO +logging.level.guru.springframework.controllers=DEBUG +logging.level.org.hibernate=INFO +logging.level.de.thpeetz=DEBUG + +jwt.auth.secret=J6GOtcwC2NJI1l0VkHu20PacPFGTxpirBxWwynoHjsc= From 1a7da0ab9f2ed890b719ec0028d8a375242f19f5 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Tue, 7 Jan 2025 01:48:03 +0100 Subject: [PATCH 09/91] add column header info --- .gitignore | 1 + .gitlab-ci.yml | 36 ++ gui/model_config.py | 72 ++-- gui/table_model.py | 11 +- .../kontor/admin/SetupModuleAdmin.java | 358 +++++++++--------- .../kontor/admin/data/MetaDataColumn.java | 6 + .../admin/services/MetaDataService.java | 20 +- .../kontor/admin/views/MetaDataForm.java | 8 +- .../kontor/admin/views/MetaDataView.java | 2 +- 9 files changed, 289 insertions(+), 225 deletions(-) create mode 100644 .gitlab-ci.yml diff --git a/.gitignore b/.gitignore index 575904a..2330d74 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ __pycache__/ bonus/ icons/ icons-shadowless/ +.vscode/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..7231f34 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,36 @@ +image: gradle:8.6-jdk21-alpine + +stages: + - build + - test + - publish + +# Disable the Gradle daemon for Continuous Integration servers as correctness +# is usually a priority over speed in CI environments. Using a fresh +# runtime for each build is more reliable since the runtime is completely +# isolated from any previous builds. +variables: + GRADLE_OPTS: "-Dorg.gradle.daemon=false" + +before_script: + - GRADLE_USER_HOME="$(pwd)/.gradle" + - export GRADLE_USER_HOME + +build: + stage: build + script: + - cd springboot + - gradle assemble --no-daemon + +test: + stage: test + script: + - cd springboot + - gradle check --no-daemon + +publish: + stage: publish + script: + - cd springboot + - gradle --no-daemon publish -PgitlabPackageRegistryUsername=gitlab-ci-token -PgitlabPackageRegistryPassword="$CI_JOB_TOKEN" + diff --git a/gui/model_config.py b/gui/model_config.py index 1bea8a5..a9c6702 100644 --- a/gui/model_config.py +++ b/gui/model_config.py @@ -5,10 +5,11 @@ from PySide6.QtWidgets import QHBoxLayout, QCheckBox class KontorModelConfig: def __init__(self, db_config, main_window, table_name: str): - self.header = [] + self.header = {} self.filter = {} self.main_window = main_window self._table = table_name + self._table_id = None self.db_conn = mariadb.connect( host=db_config['mariadb']['host'], port=db_config['mariadb']['port'], @@ -19,64 +20,65 @@ class KontorModelConfig: self.get_table_config() def get_table_id(self): + if self._table_id is not None: + return cursor = self.db_conn.cursor() cursor.execute("SELECT id, created_date, last_modified_date FROM meta_data_table WHERE table_name=?", (self._table, )) rows = cursor.fetchall() if len(rows) == 1: - return rows[0][0] - return None + self._table_id = rows[0][0] def get_table_config(self): - table_id = self.get_table_id() + if self._table_id is None: + self.get_table_id() cursor = self.db_conn.cursor() - cursor.execute("SELECT id, column_name, column_order FROM meta_data_column WHERE table_id=? AND is_shown is true", (table_id, )) + cursor.execute("SELECT column_name, column_order, column_label FROM meta_data_column WHERE table_id=? AND is_shown is true ORDER bY column_order", (self._table_id, )) rows = cursor.fetchall() self.header.clear() - for (column_id, column_name, column_order) in rows: - self.header.insert(column_order-1, column_name) - print(f"retrieved {len(rows)} columns, set {len(self.header)} headers") - - def get_header(self) -> list: - self.get_table_config() - return self.header + order = 0 + for (column_name, column_order, column_label) in rows: + self.header[order] = { 'column': column_name, 'label': column_label, 'order': column_order} + order += 1 + # print(f"retrieved {len(rows)} columns, set {len(self.header)} headers") + cursor.execute("SELECT column_name, filter_label from meta_data_column WHERE table_id=? AND show_filter is true", (self._table_id, )) + rows = cursor.fetchall() + for row in rows: + self.filter[row[0]] = {'label': row[1], 'widget': None} + # print(f"retrieved {len(rows)} filters: {self.filter}") def get_filter(self) -> str: filter_rule = "" # print(self.filter["download"].isChecked()) - if self.filter["download"].isChecked(): - # print(self.filter["download"].isChecked()) - filter_rule = "WHERE should_download is true" - if self.filter["review"].isChecked(): - if len(filter_rule) > 0: - filter_rule += " AND " - else: - filter_rule += "WHERE " - filter_rule += "review is true" - print(f"{filter_rule=}") + for column, filter_info in self.filter.items(): + # print(column, filter_info) + if filter_info['widget'].isChecked(): + # print(column, filter_info, filter_rule, len(filter_rule)) + if len(filter_rule) < 1: + filter_rule += "WHERE " + if len(filter_rule) > 8: + filter_rule += " AND " + filter_rule += f"{column} is true" + # print(f"{filter_rule=}") return filter_rule def get_statement(self) -> str: filter_rule = self.get_filter() - self.get_table_config() + # self.get_table_config() columns = "" - for index in range(len(self.header)): + for index, column in self.header.items(): if index > 0: columns += ", " - columns += self.header[index] + columns += column['column'] statement = f"SELECT {columns} FROM media_file {filter_rule}" return statement def get_filter_layout(self) -> QHBoxLayout: filter_layout = QHBoxLayout() - download_checkbox = QCheckBox() - download_checkbox.setText("Download") - download_checkbox.checkStateChanged.connect(self.main_window.refresh) - self.filter["download"] = download_checkbox - review_checkbox = QCheckBox() - review_checkbox.setText("Review") - review_checkbox.checkStateChanged.connect(self.main_window.refresh) - self.filter["review"] = review_checkbox - filter_layout.addWidget(review_checkbox) - filter_layout.addWidget(download_checkbox) + for column, filter_info in self.filter.items(): + filter_checkbox = QCheckBox() + filter_checkbox.setText(filter_info['label']) + filter_checkbox.checkStateChanged.connect(self.main_window.refresh) + self.filter[column]['widget'] = filter_checkbox + filter_layout.addWidget(filter_checkbox) filter_layout.addStretch() return filter_layout diff --git a/gui/table_model.py b/gui/table_model.py index 5fc5dc9..81eec89 100644 --- a/gui/table_model.py +++ b/gui/table_model.py @@ -19,7 +19,7 @@ class KontorTableModel(QAbstractTableModel): cursor = self._config.db_conn.cursor() cursor.execute(self._config.get_statement()) rows = cursor.fetchall() - print(len(rows)) + # print(len(rows)) if len(rows) > 0: self.beginResetModel() for row in rows: @@ -39,7 +39,7 @@ class KontorTableModel(QAbstractTableModel): def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: - return self._config.header[col] + return self._config.header[col]['column'] if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: return str(col+1) @@ -48,6 +48,7 @@ class KontorTableModel(QAbstractTableModel): return None value = self._data[index.row()][index.column()] if role == Qt.ItemDataRole.DisplayRole: + # print('{}: {}'.format(value, type(value))) if isinstance(value, datetime): return value.strftime("%Y-%m-%d %M:%M:%S") if isinstance(value, str): @@ -57,7 +58,7 @@ class KontorTableModel(QAbstractTableModel): return self._main_window.tick else: return self._main_window.cross - return value + return str(value) if role == Qt.ItemDataRole.DecorationRole: if isinstance(value, bytes): # print('{}: {}'.format(value, type(value))) @@ -69,8 +70,8 @@ class KontorTableModel(QAbstractTableModel): def columnCount(self, index=QModelIndex()): # The following takes the first sub-list, and returns # the length (only works if all rows are an equal length) - print(f"Header count: {len(self._config.get_header())}") - return len(self._config.get_header()) + # print(f"Header count: {len(self._config.get_header())}") + return len(self._config.header) def setData(self, index, value, role=Qt.ItemDataRole.EditRole): if role == Qt.ItemDataRole.EditRole: diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java index 419c519..de81e3b 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java @@ -128,209 +128,209 @@ public class SetupModuleAdmin implements ApplicationListener column.getColumnName().equals(columnName))) { log.info("Column {} with name {} of table {} found, check Values", columnOrder, columnName, table.getTableName()); MetaDataColumn column = table.getTableColumns().get(columnOrder.intValue()-1); @@ -54,9 +54,21 @@ public class MetaDataService { log.debug("columnModifier has to be changed to {}", columnModifier); column.setColumnModifier(columnModifier); } - if (column.getIsShown() == null) { - log.debug("isShown set to false"); - column.setIsShown(Boolean.FALSE); + if (!column.getIsShown().equals(isShown)) { + log.debug("isShown has to be change to {}}", isShown); + column.setIsShown(isShown); + } + if (columnLabel != null && !columnLabel.equals(column.getColumnLabel())) { + log.debug("columnLabel has to be change to {}}", columnLabel); + column.setColumnLabel(columnLabel); + } + if (showFilter != null &&!showFilter.equals(column.getShowFilter())) { + log.debug("showFilter has to be change to {}}", showFilter); + column.setShowFilter(showFilter); + } + if (filterLabel != null && !filterLabel.equals(column.getFilterLabel())) { + log.debug("filterLabel has to be change to {}}", filterLabel); + column.setFilterLabel(filterLabel); } metaDataColumnRepository.save(column); } else { diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java index f70c161..19d4459 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java @@ -26,6 +26,7 @@ public class MetaDataForm extends FormLayout { TextField columnModifier = new TextField("Column Modifier"); IntegerField columnOrder = new IntegerField("Column Order"); Checkbox isShown = new Checkbox("Is Shown"); + Checkbox showFilter = new Checkbox("Show Filter"); Button save = new com.vaadin.flow.component.button.Button("Save"); Button delete = new com.vaadin.flow.component.button.Button("Delete"); @@ -39,7 +40,12 @@ public class MetaDataForm extends FormLayout { table.setItems(tables); table.setItemLabelGenerator(MetaDataTable::getTableName); - add(table, columnName, columnSyncName, columnModifier, columnOrder, isShown, createButtonsLayout()); + add(table, 2); + add(columnName, 2); + add(columnSyncName, 2); + add(columnModifier, 2); + add(columnOrder, 2); + add(isShown, showFilter, createButtonsLayout()); } private HorizontalLayout createButtonsLayout() { diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java index 661d4a3..f3b2509 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java @@ -42,7 +42,7 @@ public class MetaDataView extends VerticalLayout { private void configureGrid() { grid.addClassName("metadata-grid"); grid.setSizeFull(); - grid.setColumns("table.tableName", "columnName", "columnSyncName", "columnModifier", "columnOrder", "isShown"); + grid.setColumns("table.tableName", "columnName", "columnSyncName", "columnModifier", "columnOrder", "isShown", "showFilter"); grid.getColumns().forEach(col -> col.setAutoWidth(true)); grid.asSingleSelect().addValueChangeListener(event -> editMetaData(event.getValue())); } From c7691153310d061a08e66e89c3206d4006a8e393 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Tue, 7 Jan 2025 21:18:14 +0100 Subject: [PATCH 10/91] implement general data tab view --- gui/data.py | 79 +++++++++++++++++ gui/dialogs.py | 85 +++++++++++++++++++ gui/kontor.py | 54 +++++++----- gui/model_config.py | 48 +++-------- gui/table_model.py | 24 +++--- .../kontor/admin/SetupModuleAdmin.java | 30 +++---- 6 files changed, 236 insertions(+), 84 deletions(-) create mode 100644 gui/data.py create mode 100644 gui/dialogs.py diff --git a/gui/data.py b/gui/data.py new file mode 100644 index 0000000..bb4436c --- /dev/null +++ b/gui/data.py @@ -0,0 +1,79 @@ +import mariadb + + +class KontorDB: + + def __init__(self, db_config): + self.db_conn = mariadb.connect( + host=db_config['mariadb']['host'], + port=db_config['mariadb']['port'], + user=db_config['mariadb']['user'], + password=db_config['mariadb']['password'], + database=db_config['mariadb']['database'] + ) + + def get_table_id(self, table_name): + cursor = self.db_conn.cursor() + cursor.execute("SELECT id, created_date, last_modified_date FROM meta_data_table WHERE table_name=?", (table_name, )) + row = cursor.fetchone() + cursor.close() + return row[0] + + def get_table_names(self) -> list: + tables_names = [] + cursor = self.db_conn.cursor() + cursor.execute("SELECT id, table_name from meta_data_table") + rows = cursor.fetchall() + for (_, table_name) in rows: + tables_names.append(table_name) + cursor.close() + return tables_names + + def get_column_meta_data(self, table_id): + cursor = self.db_conn.cursor() + meta_data = {} + cursor.execute("SELECT column_name, column_order, column_label FROM meta_data_column WHERE table_id=? AND is_shown is true ORDER bY column_order", (table_id, )) + rows = cursor.fetchall() + order = 0 + for (column_name, column_order, column_label) in rows: + meta_data[order] = { 'column': column_name, 'label': column_label, 'order': column_order} + order += 1 + cursor.close() + # print(f"retrieved {len(rows)} columns, set {len(meta_data)} headers") + return meta_data + + def get_filters(self, table_id): + cursor = self.db_conn.cursor() + filters = {} + cursor.execute("SELECT column_name, filter_label from meta_data_column WHERE table_id=? AND show_filter is true", (table_id, )) + rows = cursor.fetchall() + for row in rows: + filters[row[0]] = {'label': row[1], 'widget': None} + cursor.close() + # print(f"retrieved {len(rows)} filters: {filters}") + return filters + + def get_data(self, table_name: str, columns: dict, where_clause: str) -> list: + data = [] + cursor = self.db_conn.cursor() + cursor.execute(self.get_statement(table_name, columns, where_clause)) + rows = cursor.fetchall() + print(len(rows)) + for row in rows: + # print(f"KontorDB.get_data: {row}") + data.append(list(row)) + cursor.close() + print(f"KontorDB.getData: return {len(data)}") + return data + + def get_statement(self, table: str, header: dict, where_clause): + columns = "" + for index, column in header.items(): + if index > 0: + columns += ", " + columns += column['column'] + if len(columns) == 0: + columns = "*" + statement = f"SELECT {columns} FROM {table} {where_clause}" + print(f"{statement=}") + return statement diff --git a/gui/dialogs.py b/gui/dialogs.py new file mode 100644 index 0000000..ae69c3d --- /dev/null +++ b/gui/dialogs.py @@ -0,0 +1,85 @@ +from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QFileDialog, \ + QGroupBox, QCheckBox + + +class ExportKontorDialog(QDialog): + def __init__(self, parent=None, kontor_db=None): + super().__init__(parent) + + self.parent = parent + self.kontor_db = kontor_db + self.file_name = None + self.tables = [] + self._table_options = {} + + buttons = (QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + + self.buttonBox = QDialogButtonBox(buttons) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + self.label = QLabel() + self.label.setText("Export DB to data.json") + layout = QVBoxLayout() + file_layout = QHBoxLayout() + file_layout.addWidget(self.label) + file_button = QPushButton("Select file") + file_button.clicked.connect(self.select_file) + file_layout.addWidget(file_button) + layout.addLayout(file_layout) + + for table_name in self.kontor_db.get_table_names(): + check_box = QCheckBox(table_name) + self._table_options[table_name] = check_box + check_box.stateChanged.connect(self.change_selection) + layout.addWidget(check_box) + layout.addWidget(self.buttonBox) + self.setLayout(layout) + + def change_selection(self): + self.tables.clear() + for (name, box) in self._table_options.items(): + if box.isChecked(): + self.tables.append(name) + + def select_file(self): + file_dialog = QFileDialog() + file_dialog.setFileMode(QFileDialog.FileMode.AnyFile) + if file_dialog.exec(): + self.file_name = file_dialog.selectedFiles()[0] + self.label.setText(f"Export DB to {self.file_name}") + + def get_tables_to_export(self) -> list: + return self.tables + + +class ImportKontorDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + + self.file_name = None + + QBtn = (QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + + self.buttonBox = QDialogButtonBox(QBtn) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + self.label = QLabel() + self.label.setText("Import DB from data.json") + layout = QVBoxLayout() + file_layout = QHBoxLayout() + file_layout.addWidget(self.label) + file_button = QPushButton("Select file") + file_button.clicked.connect(self.select_file) + file_layout.addWidget(file_button) + layout.addLayout(file_layout) + layout.addWidget(self.buttonBox) + self.setLayout(layout) + + def select_file(self): + file_dialog = QFileDialog() + file_dialog.setFileMode(QFileDialog.FileMode.ExistingFile) + if file_dialog.exec(): + self.file_name = file_dialog.selectedFiles()[0] + self.label.setText(f"Import DB from {self.file_name}") diff --git a/gui/kontor.py b/gui/kontor.py index 3b601ad..771c466 100644 --- a/gui/kontor.py +++ b/gui/kontor.py @@ -11,6 +11,8 @@ from PySide6.QtWidgets import QApplication, QLabel, QMainWindow from platformdirs import PlatformDirs from comic_model import ComicTableModel +from dialogs import ExportKontorDialog, ImportKontorDialog +from data import KontorDB from model_config import KontorModelConfig from table_model import KontorTableModel @@ -35,12 +37,13 @@ class MainWindow(QMainWindow): self.data = [] self.filter = {} + self.kontor_db = KontorDB(config) self.central_widget = QWidget() parent_layout = QVBoxLayout() self.central_widget.setLayout(parent_layout) self.tabs = QTabWidget() - self.tabs.addTab(self.generate_tab_comics(config), "Comics") - self.tabs.addTab(self.generate_tab_media_file(config), "MediaFile") + self.tabs.addTab(self.generate_data_tab("comic"), "Comics") + self.tabs.addTab(self.generate_data_tab("media_file"), "MediaFile") self.tabs.currentChanged.connect(self._tab_changed) #label.setAlignment(Qt.AlignmentFlag.AlignCenter) parent_layout.addWidget(self.tabs) @@ -52,9 +55,13 @@ class MainWindow(QMainWindow): self.aboutAction = QAction("&Über...", self) self.aboutAction.triggered.connect(self.about) self.importAction = QAction(self.import_icon, "&Import", self) + self.importAction.triggered.connect(self.import_from_file) self.exportAction = QAction(self.export_icon, "&Export", self) + self.exportAction.triggered.connect(self.export_to_file) self.refreshAction = QAction(self.circle_icon, "&Refresh", self) self.refreshAction.triggered.connect(self.refresh) + self.updateTitleAction = QAction("&Update Titles", self) + self.downloadAction = QAction("&Download Videos", self) self.exitAction = QAction("&Beenden", self) self.exitAction.setShortcut("Alt+F4") self.exitAction.triggered.connect(self.close) @@ -70,6 +77,10 @@ class MainWindow(QMainWindow): menu_bar.addMenu(kontor_menu) kontor_menu.addAction(self.importAction) kontor_menu.addAction(self.exportAction) + media_file_menu = QMenu("&MediaFile") + media_file_menu.addAction(self.updateTitleAction) + media_file_menu.addAction(self.downloadAction) + kontor_menu.addMenu(media_file_menu) # Help menu help_menu = QMenu("&Hilfe") menu_bar.addMenu(help_menu) @@ -91,36 +102,41 @@ class MainWindow(QMainWindow): def about(self): QMessageBox.about(self.central_widget, "Über Kontor", f"Python: 3.11\nKontor: 0.1.0") + def import_from_file(self): + import_dlg = ImportKontorDialog(self) + if import_dlg.exec(): + print(f"import DB from file {import_dlg.file_name}") + else: + print("no nothing for import") + pass + + def export_to_file(self): + export_dlg = ExportKontorDialog(self, self.kontor_db) + if export_dlg.exec(): + print(export_dlg.get_tables_to_export()) + self.statusBar.showMessage(f"export DB to {export_dlg.file_name}", 3000) + else: + self.statusBar.showMessage("Export cancelled", 3000) + def refresh(self): self.data[self.tabs.currentIndex()].refresh() def _tab_changed(self, tab_index): self.data[tab_index].refresh() - def generate_tab_comics(self, db_configuration): - comic_tab = QWidget() - layout = QVBoxLayout() - comic_tab.setLayout(layout) - model = ComicTableModel(db_configuration, self) - self.data.append(model) - table_view = QTableView() - table_view.setModel(model) - layout.addWidget(table_view) - return comic_tab - - def generate_tab_media_file(self, db_configuration): - media_file_tab = QWidget() - table_config = KontorModelConfig(db_configuration, self, "media_file") + def generate_data_tab(self, table_name): + data_tab = QWidget() + table_config = KontorModelConfig(self.kontor_db, self, table_name) model = KontorTableModel(table_config) layout = QVBoxLayout() - # model = MediaFileTableModel(db_configuration, self) self.data.append(model) - media_file_tab.setLayout(layout) + data_tab.setLayout(layout) table_view = QTableView() table_view.setModel(model) layout.addLayout(table_config.get_filter_layout()) layout.addWidget(table_view) - return media_file_tab + model.refresh() + return data_tab if __name__ == '__main__': diff --git a/gui/model_config.py b/gui/model_config.py index a9c6702..ba3e510 100644 --- a/gui/model_config.py +++ b/gui/model_config.py @@ -1,50 +1,30 @@ import mariadb from PySide6.QtWidgets import QHBoxLayout, QCheckBox +from data import KontorDB + class KontorModelConfig: - def __init__(self, db_config, main_window, table_name: str): + def __init__(self, kontor_db: KontorDB, main_window, table_name: str): self.header = {} self.filter = {} self.main_window = main_window self._table = table_name self._table_id = None - self.db_conn = mariadb.connect( - host=db_config['mariadb']['host'], - port=db_config['mariadb']['port'], - user=db_config['mariadb']['user'], - password=db_config['mariadb']['password'], - database=db_config['mariadb']['database'] - ) + self.kontor_db = kontor_db self.get_table_config() def get_table_id(self): if self._table_id is not None: return - cursor = self.db_conn.cursor() - cursor.execute("SELECT id, created_date, last_modified_date FROM meta_data_table WHERE table_name=?", (self._table, )) - rows = cursor.fetchall() - if len(rows) == 1: - self._table_id = rows[0][0] + self._table_id = self.kontor_db.get_table_id(self._table) def get_table_config(self): if self._table_id is None: self.get_table_id() - cursor = self.db_conn.cursor() - cursor.execute("SELECT column_name, column_order, column_label FROM meta_data_column WHERE table_id=? AND is_shown is true ORDER bY column_order", (self._table_id, )) - rows = cursor.fetchall() - self.header.clear() - order = 0 - for (column_name, column_order, column_label) in rows: - self.header[order] = { 'column': column_name, 'label': column_label, 'order': column_order} - order += 1 - # print(f"retrieved {len(rows)} columns, set {len(self.header)} headers") - cursor.execute("SELECT column_name, filter_label from meta_data_column WHERE table_id=? AND show_filter is true", (self._table_id, )) - rows = cursor.fetchall() - for row in rows: - self.filter[row[0]] = {'label': row[1], 'widget': None} - # print(f"retrieved {len(rows)} filters: {self.filter}") + self.header = self.kontor_db.get_column_meta_data(self._table_id) + self.filter = self.kontor_db.get_filters(self._table_id) def get_filter(self) -> str: filter_rule = "" @@ -61,16 +41,10 @@ class KontorModelConfig: # print(f"{filter_rule=}") return filter_rule - def get_statement(self) -> str: - filter_rule = self.get_filter() - # self.get_table_config() - columns = "" - for index, column in self.header.items(): - if index > 0: - columns += ", " - columns += column['column'] - statement = f"SELECT {columns} FROM media_file {filter_rule}" - return statement + def get_data(self) -> list: + data = self.kontor_db.get_data(self._table, self.header, self.get_filter()) + print(f"KontorModelConfig.get_data: {len(data)}") + return data def get_filter_layout(self) -> QHBoxLayout: filter_layout = QHBoxLayout() diff --git a/gui/table_model.py b/gui/table_model.py index 81eec89..a28e1cc 100644 --- a/gui/table_model.py +++ b/gui/table_model.py @@ -12,24 +12,22 @@ class KontorTableModel(QAbstractTableModel): super().__init__() self._main_window = model_config.main_window self._config = model_config - self._data = None + self._data = [] def refresh(self): - data = [] - cursor = self._config.db_conn.cursor() - cursor.execute(self._config.get_statement()) - rows = cursor.fetchall() - # print(len(rows)) - if len(rows) > 0: + data = self._config.get_data() + count = 0 + # print(data) + if data is not None: self.beginResetModel() - for row in rows: - data.append(list(row)) + self._data.clear() self._data = data self.endResetModel() - else: - self._data = None + count = len(data) + # print(data) + # print(self._data) self.layoutChanged.emit() - self._main_window.statusBar.showMessage(f"{len(rows)} Einträge geladen", 3000) + self._main_window.statusBar.showMessage(f"{count} Einträge geladen", 3000) def rowCount(self, parent=QModelIndex()): # The length of the outer list. @@ -39,7 +37,7 @@ class KontorTableModel(QAbstractTableModel): def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: - return self._config.header[col]['column'] + return self._config.header[col]['label'] if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: return str(col+1) diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java index de81e3b..81c6c6a 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java @@ -128,22 +128,22 @@ public class SetupModuleAdmin implements ApplicationListener Date: Wed, 8 Jan 2025 08:05:09 +0100 Subject: [PATCH 11/91] prevent NPE --- .../java/de/thpeetz/kontor/admin/services/MetaDataService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java b/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java index 904a7e1..3f1c773 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java @@ -54,7 +54,7 @@ public class MetaDataService { log.debug("columnModifier has to be changed to {}", columnModifier); column.setColumnModifier(columnModifier); } - if (!column.getIsShown().equals(isShown)) { + if (isShown != null && !isShown.equals(column.getIsShown())) { log.debug("isShown has to be change to {}}", isShown); column.setIsShown(isShown); } From 8d31a926926fd10e8357ec6565625f09388f55ef Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Wed, 8 Jan 2025 10:39:52 +0100 Subject: [PATCH 12/91] improve view of meta data by using icons for boolean and add search --- .../kontor/admin/SetupModuleAdmin.java | 2 +- .../kontor/admin/data/MetaDataColumn.java | 4 + .../admin/data/MetaDataColumnRepository.java | 6 + .../admin/services/MetaDataService.java | 13 +- .../kontor/admin/views/MetaDataForm.java | 14 ++- .../kontor/admin/views/MetaDataView.java | 117 +++++++++++++++++- .../media/services/MediaFileService.java | 4 +- 7 files changed, 143 insertions(+), 17 deletions(-) diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java index 81c6c6a..4ac89e0 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java @@ -177,7 +177,7 @@ public class SetupModuleAdmin implements ApplicationListener { List findByTable(MetaDataTable table); + + @Query("select m from MetaDataColumn m " + + "where lower(m.columnName) like lower(concat('%', :searchTerm, '%')) or lower(m.columnLabel) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); } diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java b/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java index 3f1c773..98ae1c3 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java @@ -36,7 +36,7 @@ public class MetaDataService { public void getColumn(MetaDataTable table, String columnName, String columnSyncName, String columnType, String columnModifier, Integer columnOrder, Boolean isShown, String columnLabel, Boolean showFilter, String filterLabel) { if (table.getTableColumns().stream().anyMatch(column -> column.getColumnName().equals(columnName))) { - log.info("Column {} with name {} of table {} found, check Values", columnOrder, columnName, table.getTableName()); + log.debug("Column {} with name {} of table {} found, check Values", columnOrder, columnName, table.getTableName()); MetaDataColumn column = table.getTableColumns().get(columnOrder.intValue()-1); if (!column.getColumnName().equals(columnName)) { log.debug("columnName has to be changed to {}", columnName); @@ -85,8 +85,15 @@ public class MetaDataService { } } - public List findAllMetaDataColumns() { - return metaDataColumnRepository.findAll(); + public List findAllMetaDataColumns(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + log.debug("Found " + metaDataColumnRepository.count()+ " entries"); + return metaDataColumnRepository.findAll(); + } else { + List results = metaDataColumnRepository.search(stringFilter); + log.debug("Found " + results.size() + " entries"); + return results; + } } public void deleteMetaDataColumn(MetaDataColumn metaDataColumn) { diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java index 19d4459..b65b099 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java @@ -26,7 +26,9 @@ public class MetaDataForm extends FormLayout { TextField columnModifier = new TextField("Column Modifier"); IntegerField columnOrder = new IntegerField("Column Order"); Checkbox isShown = new Checkbox("Is Shown"); + TextField columnLabel = new TextField("Column Label"); Checkbox showFilter = new Checkbox("Show Filter"); + TextField filterLabel = new TextField("Filter Label"); Button save = new com.vaadin.flow.component.button.Button("Save"); Button delete = new com.vaadin.flow.component.button.Button("Delete"); @@ -40,12 +42,12 @@ public class MetaDataForm extends FormLayout { table.setItems(tables); table.setItemLabelGenerator(MetaDataTable::getTableName); - add(table, 2); - add(columnName, 2); - add(columnSyncName, 2); - add(columnModifier, 2); - add(columnOrder, 2); - add(isShown, showFilter, createButtonsLayout()); + add(table, columnName, columnSyncName, columnModifier, columnOrder); + add(isShown, columnLabel); + isShown.addClickListener(click -> columnLabel.setEnabled(isShown.getValue())); + add(showFilter, filterLabel); + showFilter.addClickListener(click -> filterLabel.setEnabled(showFilter.getValue())); + add(createButtonsLayout()); } private HorizontalLayout createButtonsLayout() { diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java index f3b2509..780126a 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java @@ -2,9 +2,18 @@ package de.thpeetz.kontor.admin.views; import com.vaadin.flow.component.Component; import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.contextmenu.ContextMenu; +import com.vaadin.flow.component.contextmenu.MenuItem; import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.GridSortOrder; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.provider.SortDirection; +import com.vaadin.flow.data.value.ValueChangeMode; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; import com.vaadin.flow.spring.annotation.SpringComponent; @@ -13,9 +22,13 @@ import de.thpeetz.kontor.admin.data.MetaDataColumn; import de.thpeetz.kontor.admin.services.MetaDataService; import de.thpeetz.kontor.common.views.MainLayout; import jakarta.annotation.security.RolesAllowed; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Scope; +import java.util.ArrayList; +import java.util.List; + @Slf4j @SpringComponent @Scope("prototype") @@ -24,10 +37,56 @@ import org.springframework.context.annotation.Scope; @PageTitle("Meta Data | Admin | Kontor") public class MetaDataView extends VerticalLayout { - Grid grid = new Grid<>(MetaDataColumn.class); + Grid grid = new Grid<>(MetaDataColumn.class, false); + Grid.Column idColumn = grid.addColumn(MetaDataColumn::getId) + .setHeader("ID").setResizable(true).setSortable(true); + Grid.Column createdColumn = grid.addColumn(MetaDataColumn::getCreatedDate) + .setHeader("Erstellt").setResizable(true).setSortable(true); + Grid.Column modifiedColumn = grid.addColumn(MetaDataColumn::getLastModifiedDate) + .setHeader("Geändert").setResizable(true).setSortable(true); + Grid.Column versionColumn = grid.addColumn(MetaDataColumn::getVersion) + .setHeader("Version").setResizable(true).setSortable(true); + Grid.Column tableColumn = grid.addColumn(MetaDataColumn::getTableName) + .setHeader("Table").setResizable(true).setSortable(true); + Grid.Column columnNameColumn = grid.addColumn(MetaDataColumn::getColumnName) + .setHeader("Column Name").setResizable(true).setSortable(true); + Grid.Column columnSyncNameColumn = grid.addColumn(MetaDataColumn::getColumnSyncName) + .setHeader("Column Sync Name").setResizable(true).setSortable(true); + Grid.Column columnTypeColumn = grid.addColumn(MetaDataColumn::getColumnType) + .setHeader("Column Type").setResizable(true).setSortable(true); + Grid.Column columnModifierColumn = grid.addColumn(MetaDataColumn::getColumnModifier) + .setHeader("Column Modifier").setResizable(true).setSortable(true); + Grid.Column columnOrderColumn = grid.addColumn(MetaDataColumn::getColumnOrder) + .setHeader("Column Order").setResizable(true).setSortable(true); + Grid.Column isShownColumn = grid.addComponentColumn(metaDataColumn -> createStatusIcon(metaDataColumn.getIsShown())). + setHeader("Anzeige?").setWidth("6rem").setSortable(true); + Grid.Column columnLabelColumn = grid.addColumn(MetaDataColumn::getColumnLabel) + .setHeader("Spaltenname").setResizable(true).setSortable(true); + Grid.Column showFilterColumn = grid.addComponentColumn(metaDataColumn -> createStatusIcon(metaDataColumn.getShowFilter())). + setHeader("Zeige Filter").setWidth("6rem").setSortable(true); + Grid.Column filterLabelColumn = grid.addColumn(MetaDataColumn::getFilterLabel) + .setHeader("Filter Name").setResizable(true).setSortable(true); + TextField searchField = new TextField(); + @Getter MetaDataForm form; MetaDataService service; + private static class ColumnToggleContextMenu extends ContextMenu { + public ColumnToggleContextMenu(Component target) { + super(target); + setOpenOnClick(true); + } + + void addColumnToggleItem(String label, Grid.Column column) { + MenuItem menuItem = this.addItem(label, e -> { + column.setVisible(e.getSource().isChecked()); + }); + menuItem.setCheckable(true); + menuItem.setChecked(column.isVisible()); + menuItem.setKeepOpen(true); + } + } + public MetaDataView(MetaDataService service) { this.service = service; addClassName("metadata-view"); @@ -42,8 +101,17 @@ public class MetaDataView extends VerticalLayout { private void configureGrid() { grid.addClassName("metadata-grid"); grid.setSizeFull(); - grid.setColumns("table.tableName", "columnName", "columnSyncName", "columnModifier", "columnOrder", "isShown", "showFilter"); + //grid.setColumns("table.tableName", "columnName", "columnSyncName", "columnModifier", "columnOrder", "isShown", "columnLabel", "showFilter", "filterLabel"); grid.getColumns().forEach(col -> col.setAutoWidth(true)); + idColumn.setVisible(false); + createdColumn.setVisible(false); + modifiedColumn.setVisible(false); + versionColumn.setVisible(false); + grid.setMultiSort(true); + List sortOrder = new ArrayList(); + sortOrder.add(new GridSortOrder(tableColumn, SortDirection.ASCENDING)); + sortOrder.add(new GridSortOrder(columnOrderColumn, SortDirection.ASCENDING)); + grid.sort(sortOrder); grid.asSingleSelect().addValueChangeListener(event -> editMetaData(event.getValue())); } @@ -56,6 +124,19 @@ public class MetaDataView extends VerticalLayout { form.addCloseListener(e -> closeEditor()); } + private Icon createStatusIcon(boolean status) { + Icon icon; + if (status) { + icon = VaadinIcon.CHECK.create(); + icon.getElement().getThemeList().add("badge success"); + } else { + icon = VaadinIcon.CLOSE_SMALL.create(); + icon.getElement().getThemeList().add("badge error"); + } + icon.getStyle().set("padding", "var(--lumo-space-xs"); + return icon; + } + private void saveMetaData(MetaDataForm.SaveEvent event) { MetaDataColumn metaDataColumn = event.getMetaDataColumn(); service.saveMetaDataColumn(metaDataColumn); @@ -79,8 +160,34 @@ public class MetaDataView extends VerticalLayout { } private HorizontalLayout getToolbar() { - Button addAuthorizationMaxtrixButton = new Button("Add Meta Data", click -> addMetaDataColumn()); - HorizontalLayout toolbar = new HorizontalLayout(addAuthorizationMaxtrixButton); + searchField.setPlaceholder("Search"); + searchField.setClearButtonVisible(true); + searchField.setPrefixComponent(new Icon(VaadinIcon.SEARCH)); + searchField.setValueChangeMode(ValueChangeMode.EAGER); + searchField.addValueChangeListener(e -> updateList()); + + Button addMetaDataButton = new Button("Add Meta Data"); + addMetaDataButton.addClickListener(click -> addMetaDataColumn()); + + Button menuButton = new Button("Show/Hide Columns"); + menuButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + MetaDataView.ColumnToggleContextMenu columnToggleContextMenu = new MetaDataView.ColumnToggleContextMenu(menuButton); + columnToggleContextMenu.addColumnToggleItem("ID", idColumn); + columnToggleContextMenu.addColumnToggleItem("Erstellt", createdColumn); + columnToggleContextMenu.addColumnToggleItem("Geändert", modifiedColumn); + columnToggleContextMenu.addColumnToggleItem(versionColumn.getHeaderText(), versionColumn); + columnToggleContextMenu.addColumnToggleItem(tableColumn.getHeaderText(), tableColumn); + columnToggleContextMenu.addColumnToggleItem(columnNameColumn.getHeaderText(), columnNameColumn); + columnToggleContextMenu.addColumnToggleItem(columnSyncNameColumn.getHeaderText(), columnSyncNameColumn); + columnToggleContextMenu.addColumnToggleItem(columnTypeColumn.getHeaderText(), columnTypeColumn); + columnToggleContextMenu.addColumnToggleItem(columnModifierColumn.getHeaderText(), columnModifierColumn); + columnToggleContextMenu.addColumnToggleItem(columnOrderColumn.getHeaderText(), columnOrderColumn); + columnToggleContextMenu.addColumnToggleItem(isShownColumn.getHeaderText(), isShownColumn); + columnToggleContextMenu.addColumnToggleItem(columnLabelColumn.getHeaderText(), columnLabelColumn); + columnToggleContextMenu.addColumnToggleItem(showFilterColumn.getHeaderText(), showFilterColumn); + columnToggleContextMenu.addColumnToggleItem(filterLabelColumn.getHeaderText(), filterLabelColumn); + + HorizontalLayout toolbar = new HorizontalLayout(searchField, addMetaDataButton, menuButton); toolbar.addClassName("toolbar"); return toolbar; } @@ -107,6 +214,6 @@ public class MetaDataView extends VerticalLayout { } private void updateList() { - grid.setItems(service.findAllMetaDataColumns()); + grid.setItems(service.findAllMetaDataColumns(searchField.getValue())); } } diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java b/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java index a7fe0a1..ffb0a24 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java +++ b/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java @@ -19,11 +19,11 @@ public class MediaFileService { public List findAllMediaFiles(String stringFilter) { if (stringFilter == null || stringFilter.isEmpty()) { - log.info("Found " + mediaFileRepository.count()+ " entries"); + log.debug("Found " + mediaFileRepository.count()+ " entries"); return mediaFileRepository.findAll(); } else { List results = mediaFileRepository.search(stringFilter); - log.info("Found " + results.size() + " entries"); + log.debug("Found " + results.size() + " entries"); return results; } } From 24e4c0b58e61302d96dba9c9b251f301865db7de Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Wed, 8 Jan 2025 14:15:29 +0100 Subject: [PATCH 13/91] import export dialog by adding selection of export type --- gui/dialogs.py | 25 +++++++++++++++++++++++-- gui/kontor.py | 1 + 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/gui/dialogs.py b/gui/dialogs.py index ae69c3d..f7e51d1 100644 --- a/gui/dialogs.py +++ b/gui/dialogs.py @@ -1,5 +1,7 @@ +from pathlib import Path + from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QFileDialog, \ - QGroupBox, QCheckBox + QGroupBox, QCheckBox, QComboBox class ExportKontorDialog(QDialog): @@ -12,17 +14,26 @@ class ExportKontorDialog(QDialog): self.tables = [] self._table_options = {} + self.export_options = {"JSON": {"ext": ".json"}, "YAML": {"ext": ".yaml"}, "SQLite": {"ext": ".db"}} + self.current_export_type = "JSON" + buttons = (QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.buttonBox = QDialogButtonBox(buttons) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) + layout = QVBoxLayout() + self.label = QLabel() self.label.setText("Export DB to data.json") - layout = QVBoxLayout() + + self.combo_box = QComboBox() + self.combo_box.addItems(["JSON", "YAML", "SQLite"]) + self.combo_box.currentTextChanged.connect(self.change_export_type) file_layout = QHBoxLayout() file_layout.addWidget(self.label) + file_layout.addWidget(self.combo_box) file_button = QPushButton("Select file") file_button.clicked.connect(self.select_file) file_layout.addWidget(file_button) @@ -30,6 +41,8 @@ class ExportKontorDialog(QDialog): for table_name in self.kontor_db.get_table_names(): check_box = QCheckBox(table_name) + check_box.setChecked(True) + self.tables.append(table_name) self._table_options[table_name] = check_box check_box.stateChanged.connect(self.change_selection) layout.addWidget(check_box) @@ -42,11 +55,19 @@ class ExportKontorDialog(QDialog): if box.isChecked(): self.tables.append(name) + def change_export_type(self, text): + self.current_export_type = text + self.label.setText(f"Export DB to data.{self.export_options[text]["ext"]}") + def select_file(self): file_dialog = QFileDialog() file_dialog.setFileMode(QFileDialog.FileMode.AnyFile) + file_dialog.setDefaultSuffix(self.export_options[self.current_export_type]["ext"]) + file_dialog.setNameFilter(f"*{self.export_options[self.current_export_type]["ext"]}") if file_dialog.exec(): self.file_name = file_dialog.selectedFiles()[0] + export_file = Path(self.file_name) + self.file_name = export_file.with_suffix(self.export_options[self.current_export_type]["ext"]) self.label.setText(f"Export DB to {self.file_name}") def get_tables_to_export(self) -> list: diff --git a/gui/kontor.py b/gui/kontor.py index 771c466..30bb3bd 100644 --- a/gui/kontor.py +++ b/gui/kontor.py @@ -114,6 +114,7 @@ class MainWindow(QMainWindow): export_dlg = ExportKontorDialog(self, self.kontor_db) if export_dlg.exec(): print(export_dlg.get_tables_to_export()) + print(f"export DB to {export_dlg.file_name}") self.statusBar.showMessage(f"export DB to {export_dlg.file_name}", 3000) else: self.statusBar.showMessage("Export cancelled", 3000) From e078f43cc6596c22a06110a178ce5e15157506ee Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Wed, 8 Jan 2025 14:28:18 +0000 Subject: [PATCH 14/91] set correct project id in build.gradle --- springboot/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/springboot/build.gradle b/springboot/build.gradle index 8892faf..0624568 100644 --- a/springboot/build.gradle +++ b/springboot/build.gradle @@ -97,7 +97,7 @@ publishing { repositories { maven { name = "gitlabPackageRegistry" - url = uri("https://gitlab.com/api/v4/projects/62010300/packages/maven") + url = uri("https://gitlab.com/api/v4/projects/64726715/packages/maven") credentials(PasswordCredentials) } } From fc4110b11d9456603686dfe7dff26df7e33c492c Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Wed, 8 Jan 2025 15:34:11 +0100 Subject: [PATCH 15/91] fix problem in string formatting --- gui/dialogs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/dialogs.py b/gui/dialogs.py index f7e51d1..703baeb 100644 --- a/gui/dialogs.py +++ b/gui/dialogs.py @@ -57,13 +57,13 @@ class ExportKontorDialog(QDialog): def change_export_type(self, text): self.current_export_type = text - self.label.setText(f"Export DB to data.{self.export_options[text]["ext"]}") + self.label.setText(f'Export DB to data.{self.export_options[text]["ext"]}') def select_file(self): file_dialog = QFileDialog() file_dialog.setFileMode(QFileDialog.FileMode.AnyFile) file_dialog.setDefaultSuffix(self.export_options[self.current_export_type]["ext"]) - file_dialog.setNameFilter(f"*{self.export_options[self.current_export_type]["ext"]}") + file_dialog.setNameFilter(f'*{self.export_options[self.current_export_type]["ext"]}') if file_dialog.exec(): self.file_name = file_dialog.selectedFiles()[0] export_file = Path(self.file_name) From 6923b3e5eeb76dac76ff8d0498fb73af2951f3cd Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Wed, 8 Jan 2025 22:31:20 +0100 Subject: [PATCH 16/91] import from kontor-flask --- flask/config.py | 47 ++ flask/docs/Makefile | 20 + flask/docs/conf.py | 200 +++++++++ flask/docs/index.rst | 20 + flask/kontor/__init__.py | 74 ++++ flask/kontor/admin/__init__.py | 4 + flask/kontor/admin/forms.py | 17 + flask/kontor/admin/views.py | 63 +++ flask/kontor/auth/__init__.py | 4 + flask/kontor/auth/forms.py | 50 +++ flask/kontor/auth/views.py | 79 ++++ flask/kontor/comics/__init__.py | 3 + flask/kontor/comics/forms.py | 31 ++ flask/kontor/comics/models.py | 51 +++ flask/kontor/comics/views.py | 222 ++++++++++ flask/kontor/home/__init__.py | 3 + flask/kontor/home/views.py | 39 ++ flask/kontor/library/__init__.py | 3 + flask/kontor/library/forms.py | 35 ++ flask/kontor/library/models.py | 55 +++ flask/kontor/library/views.py | 228 ++++++++++ flask/kontor/models.py | 66 +++ flask/kontor/office/__init__.py | 3 + flask/kontor/static/css/style.css | 126 ++++++ flask/kontor/templates/admin/users/user.html | 21 + flask/kontor/templates/admin/users/users.html | 58 +++ flask/kontor/templates/auth/login.html | 18 + flask/kontor/templates/auth/register.html | 14 + flask/kontor/templates/base.html | 107 +++++ flask/kontor/templates/comics/artists.html | 58 +++ flask/kontor/templates/comics/comics.html | 66 +++ flask/kontor/templates/comics/publishers.html | 58 +++ .../templates/home/admin_dashboard.html | 31 ++ flask/kontor/templates/home/dashboard.html | 20 + flask/kontor/templates/home/index.html | 20 + flask/kontor/templates/library/authors.html | 58 +++ flask/kontor/templates/library/books.html | 74 ++++ .../kontor/templates/library/publishers.html | 58 +++ flask/kontor/templates/simpleform.html | 21 + .../kontor/templates/tysc/manufacturers.html | 58 +++ flask/kontor/templates/tysc/players.html | 60 +++ flask/kontor/templates/tysc/positions.html | 62 +++ flask/kontor/templates/tysc/sports.html | 58 +++ flask/kontor/templates/tysc/teams.html | 60 +++ flask/kontor/tysc/__init__.py | 377 ++++++++++++++++ flask/kontor/tysc/forms.py | 76 ++++ flask/kontor/tysc/models.py | 155 +++++++ flask/kontor/tysc/views.py | 405 ++++++++++++++++++ flask/kontor/version.py | 2 + flask/pylint.cfg | 382 +++++++++++++++++ flask/requirements.txt | 11 + flask/tests/__init__.py | 38 ++ flask/tests/test_comics_model.py | 51 +++ flask/tests/test_comics_view.py | 44 ++ flask/tests/test_config.txt | 4 + flask/tests/test_kontor_model.py | 14 + flask/tests/test_kontor_view.py | 58 +++ flask/tests/test_library_model.py | 49 +++ flask/tests/test_library_view.py | 44 ++ flask/tests/test_tysc_model.py | 53 +++ flask/tests/test_tysc_view.py | 110 +++++ 61 files changed, 4296 insertions(+) create mode 100644 flask/config.py create mode 100644 flask/docs/Makefile create mode 100644 flask/docs/conf.py create mode 100644 flask/docs/index.rst create mode 100644 flask/kontor/__init__.py create mode 100644 flask/kontor/admin/__init__.py create mode 100644 flask/kontor/admin/forms.py create mode 100644 flask/kontor/admin/views.py create mode 100644 flask/kontor/auth/__init__.py create mode 100644 flask/kontor/auth/forms.py create mode 100644 flask/kontor/auth/views.py create mode 100644 flask/kontor/comics/__init__.py create mode 100644 flask/kontor/comics/forms.py create mode 100644 flask/kontor/comics/models.py create mode 100644 flask/kontor/comics/views.py create mode 100644 flask/kontor/home/__init__.py create mode 100644 flask/kontor/home/views.py create mode 100644 flask/kontor/library/__init__.py create mode 100644 flask/kontor/library/forms.py create mode 100644 flask/kontor/library/models.py create mode 100644 flask/kontor/library/views.py create mode 100644 flask/kontor/models.py create mode 100644 flask/kontor/office/__init__.py create mode 100644 flask/kontor/static/css/style.css create mode 100644 flask/kontor/templates/admin/users/user.html create mode 100644 flask/kontor/templates/admin/users/users.html create mode 100644 flask/kontor/templates/auth/login.html create mode 100644 flask/kontor/templates/auth/register.html create mode 100644 flask/kontor/templates/base.html create mode 100644 flask/kontor/templates/comics/artists.html create mode 100644 flask/kontor/templates/comics/comics.html create mode 100644 flask/kontor/templates/comics/publishers.html create mode 100644 flask/kontor/templates/home/admin_dashboard.html create mode 100644 flask/kontor/templates/home/dashboard.html create mode 100644 flask/kontor/templates/home/index.html create mode 100644 flask/kontor/templates/library/authors.html create mode 100644 flask/kontor/templates/library/books.html create mode 100644 flask/kontor/templates/library/publishers.html create mode 100644 flask/kontor/templates/simpleform.html create mode 100644 flask/kontor/templates/tysc/manufacturers.html create mode 100644 flask/kontor/templates/tysc/players.html create mode 100644 flask/kontor/templates/tysc/positions.html create mode 100644 flask/kontor/templates/tysc/sports.html create mode 100644 flask/kontor/templates/tysc/teams.html create mode 100644 flask/kontor/tysc/__init__.py create mode 100644 flask/kontor/tysc/forms.py create mode 100644 flask/kontor/tysc/models.py create mode 100644 flask/kontor/tysc/views.py create mode 100644 flask/kontor/version.py create mode 100644 flask/pylint.cfg create mode 100644 flask/requirements.txt create mode 100644 flask/tests/__init__.py create mode 100644 flask/tests/test_comics_model.py create mode 100644 flask/tests/test_comics_view.py create mode 100644 flask/tests/test_config.txt create mode 100644 flask/tests/test_kontor_model.py create mode 100644 flask/tests/test_kontor_view.py create mode 100644 flask/tests/test_library_model.py create mode 100644 flask/tests/test_library_view.py create mode 100644 flask/tests/test_tysc_model.py create mode 100644 flask/tests/test_tysc_view.py diff --git a/flask/config.py b/flask/config.py new file mode 100644 index 0000000..4ea5e78 --- /dev/null +++ b/flask/config.py @@ -0,0 +1,47 @@ +# config.py + + +class Config(object): + """ + Common configurations + """ + + # Put any configurations here that are common across all environments + host = '127.0.0.1' + port = 8500 + database = 'kontor' + + +class DevelopmentConfig(Config): + """ + Development configurations + """ + + DEBUG = True + host = '0.0.0.0' + database = 'kontor_dev' + + +class ProductionConfig(Config): + """ + Production configurations + """ + + DEBUG = False + +class TestingConfig(Config): + """ + Testing configurations + """ + + TESTING = True + DEBUG = True + port = 8600 + database = 'kontor_test' + +app_config = { + 'development': DevelopmentConfig, + 'production': ProductionConfig, + 'testing': TestingConfig +} + diff --git a/flask/docs/Makefile b/flask/docs/Makefile new file mode 100644 index 0000000..d7218b4 --- /dev/null +++ b/flask/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = KontorFlask +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/flask/docs/conf.py b/flask/docs/conf.py new file mode 100644 index 0000000..7278f2d --- /dev/null +++ b/flask/docs/conf.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Kontor Flask documentation build configuration file, created by +# sphinx-quickstart on Mon Nov 6 08:53:04 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.mathjax', + 'sphinx.ext.ifconfig', + 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'Kontor Flask' +copyright = '2017, Thomas Peetz' +author = 'Thomas Peetz' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.1' +# The full version, including alpha/beta/rc tags. +release = '0.0.7' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'de' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# This is required for the alabaster theme +# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars +html_sidebars = { + '**': [ + 'relations.html', # needs 'show_related': True theme option to display + 'searchbox.html', + ] +} + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'KontorFlaskdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'KontorFlask.tex', 'Kontor Flask Documentation', + 'Thomas Peetz', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'kontorflask', 'Kontor Flask Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'KontorFlask', 'Kontor Flask Documentation', + author, 'KontorFlask', 'One line description of project.', + 'Miscellaneous'), +] + + + +# -- Options for Epub output ---------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project +epub_author = author +epub_publisher = author +epub_copyright = copyright + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'https://docs.python.org/': None} diff --git a/flask/docs/index.rst b/flask/docs/index.rst new file mode 100644 index 0000000..ca4df98 --- /dev/null +++ b/flask/docs/index.rst @@ -0,0 +1,20 @@ +.. Kontor Flask documentation master file, created by + sphinx-quickstart on Mon Nov 6 08:53:04 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Kontor Flask's documentation! +======================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/flask/kontor/__init__.py b/flask/kontor/__init__.py new file mode 100644 index 0000000..961e132 --- /dev/null +++ b/flask/kontor/__init__.py @@ -0,0 +1,74 @@ +""" +Module kontor implements Kontor application. +""" + +from flask import Flask +from flask_bootstrap import Bootstrap +from flask_login import LoginManager +from pymodm import connect + +# local imports +from config import app_config +from .version import __version__ + + +_LOGIN_MANAGER_ = LoginManager() + + +def get_host(config_name): + """ + Returns host address from configuration. + :param config_name: + :return: host address + """ + host = app_config[config_name].host + return host + + +def get_port(config_name): + """ + Returns port number from configuration. + :param config_name: + :return: port number + """ + port = app_config[config_name].port + return port + + +def create_app(config_name): + """ + Returns Flask application. + :param config_name: + :return: Flask application + """ + app = Flask(__name__, instance_relative_config=True) + app.config.from_object(app_config[config_name]) + app.config.from_pyfile('config.py') + + database = "mongodb://localhost:27017/{}".format(app_config[config_name].database) + connect(database, alias="kontor") + + Bootstrap(app) + _LOGIN_MANAGER_.init_app(app) + _LOGIN_MANAGER_.login_message = "You must be logged in to access this page." + _LOGIN_MANAGER_.login_view = "auth.login" + + from .admin.views import ADMIN as ADMIN_BLUEPRINT + app.register_blueprint(ADMIN_BLUEPRINT, url_prefix='/admin') + + from .auth.views import AUTH as AUTH_BLUEPRINT + app.register_blueprint(AUTH_BLUEPRINT) + + from .home.views import HOME as HOME_BLUEPRINT + app.register_blueprint(HOME_BLUEPRINT) + + from .comics.views import COMIC as COMIC_BLUEPRINT + app.register_blueprint(COMIC_BLUEPRINT, url_prefix='/comics') + + from .library.views import LIBRARY as LIBRARY_BLUEPRINT + app.register_blueprint(LIBRARY_BLUEPRINT, url_prefix='/library') + + from .tysc.views import TYSC as TYSC_BLUEPRINT + app.register_blueprint(TYSC_BLUEPRINT, url_prefix='/tysc') + + return app diff --git a/flask/kontor/admin/__init__.py b/flask/kontor/admin/__init__.py new file mode 100644 index 0000000..ca00a4b --- /dev/null +++ b/flask/kontor/admin/__init__.py @@ -0,0 +1,4 @@ +""" +Module admin implements administration functions. +""" +from . import views diff --git a/flask/kontor/admin/forms.py b/flask/kontor/admin/forms.py new file mode 100644 index 0000000..d05cfaf --- /dev/null +++ b/flask/kontor/admin/forms.py @@ -0,0 +1,17 @@ +""" +Define form to edit users +""" +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField +from wtforms.validators import DataRequired + + +class UserEditForm(FlaskForm): + """ + Form for admin to edit users + """ + email = StringField('Email', validators=[DataRequired()]) + username = StringField('Username', validators=[DataRequired()]) + first_name = StringField('First Name', validators=[DataRequired()]) + last_name = StringField('Last Name', validators=[DataRequired()]) + submit = SubmitField('Submit') diff --git a/flask/kontor/admin/views.py b/flask/kontor/admin/views.py new file mode 100644 index 0000000..e96d8f0 --- /dev/null +++ b/flask/kontor/admin/views.py @@ -0,0 +1,63 @@ +""" +Define routing rules for managing users +""" +from flask import abort, Blueprint, flash, redirect, render_template, url_for +from flask_login import current_user, login_required + +from .forms import UserEditForm +from ..models import User + +ADMIN = Blueprint('admin', __name__) + + +def check_admin(): + """ + Prevent non-admins from accessing the page + """ + if not current_user.is_admin: + abort(403) + + +# User Views +@ADMIN.route('/users') +@login_required +def list_users(): + """ + List all users + """ + check_admin() + + users = User.objects.all() + return render_template('admin/users/users.html', + users=users, title='Users') + + +@ADMIN.route('/users/edit/', methods=['GET', 'POST']) +@login_required +def edit_user(user_id): + """ + Edit an user. + """ + check_admin() + + user = User.objects.get({'username': user_id}) + + # prevent admin from being assigned a department or role + if user.is_admin: + abort(403) + + form = UserEditForm(obj=user) + if form.validate_on_submit(): + user.email = form.email.data + user.username = form.username.data + user.first_name = form.first_name.data + user.last_name = form.last_name.data + user.save() + flash('You have successfully edited an user.') + + # redirect to the roles page + return redirect(url_for('admin.list_users')) + + return render_template('admin/users/user.html', + user=user, form=form, + title='Edit User') diff --git a/flask/kontor/auth/__init__.py b/flask/kontor/auth/__init__.py new file mode 100644 index 0000000..2d9b1ca --- /dev/null +++ b/flask/kontor/auth/__init__.py @@ -0,0 +1,4 @@ +""" +Module auth implements authentication functions. +""" +from . import views diff --git a/flask/kontor/auth/forms.py b/flask/kontor/auth/forms.py new file mode 100644 index 0000000..6e4d6dd --- /dev/null +++ b/flask/kontor/auth/forms.py @@ -0,0 +1,50 @@ +""" +Contains forms for authentication. +""" +from flask_wtf import FlaskForm +from wtforms import PasswordField, StringField, SubmitField, ValidationError +from wtforms.validators import DataRequired, Email, EqualTo + +from ..models import User + + +class RegistrationForm(FlaskForm): + """ + Form for users to create new account + """ + email = StringField('Email', validators=[DataRequired(), Email()]) + username = StringField('Username', validators=[DataRequired()]) + first_name = StringField('First Name', validators=[DataRequired()]) + last_name = StringField('Last Name', validators=[DataRequired()]) + password = PasswordField('Password', validators=[DataRequired(), + EqualTo('confirm_password') + ]) + confirm_password = PasswordField('Confirm Password') + submit = SubmitField('Register') + + def validate_email(self, field): + """ + Check if email is already in use. + :param field: + :return: + """ + if User.objects.raw({'email': field.data}).count() > 0: + raise ValidationError('Email is already in use.') + + def validate_username(self, field): + """ + check if username is already in use. + :param field: + :return: + """ + if User.objects.raw({'username': field.data}).count() > 0: + raise ValidationError('Username is already in use.') + + +class LoginForm(FlaskForm): + """ + Form for users to login + """ + email = StringField('Email', validators=[DataRequired(), Email()]) + password = PasswordField('Password', validators=[DataRequired()]) + submit = SubmitField('Login') diff --git a/flask/kontor/auth/views.py b/flask/kontor/auth/views.py new file mode 100644 index 0000000..217ae18 --- /dev/null +++ b/flask/kontor/auth/views.py @@ -0,0 +1,79 @@ +""" +Define routing rules for registering users and login and logout. +""" +from flask import Blueprint, flash, redirect, render_template, url_for +from flask_login import login_required, login_user, logout_user +from .forms import LoginForm, RegistrationForm +from ..models import User + +AUTH = Blueprint('auth', __name__) + + +@AUTH.route('/register', methods=['GET', 'POST']) +def register(): + """ + Handle requests to the /register route + Add an employee to the database through the registration form + """ + form = RegistrationForm() + if form.validate_on_submit(): + user = User() + user.email = form.email.data + user.username = form.username.data + user.first_name = form.first_name.data + user.last_name = form.last_name.data + user.password = form.password.data + # add employee to the database + user.save() + flash('You have successfully registered! You may now login.') + + # redirect to the login page + return redirect(url_for('auth.login')) + + # load registration template + return render_template('auth/register.html', form=form, title='Register') + + +@AUTH.route('/login', methods=['GET', 'POST']) +def login(): + """ + Handle requests to the /login route + Validate given email with matching password + :return: + """ + form = LoginForm() + if form.validate_on_submit(): + + # check whether employee exists in the database and whether + # the password entered matches the password in the database + user = User.objects.get({'email': form.email.data}) + if user is not None and user.verify_password( + form.password.data): + # log employee in + login_user(user) + + # redirect to the appropriate dashboard page + if user.is_admin: + return redirect(url_for('home.admin_dashboard')) + else: + return redirect(url_for('home.dashboard')) + + # when login details are incorrect + else: + flash('Invalid email or password.') + + # load login template + return render_template('auth/login.html', form=form, title='Login') + +@AUTH.route('/logout') +@login_required +def logout(): + """ + Handle requests to the /logout route + Log an employee out through the logout link + """ + logout_user() + flash('You have successfully been logged out.') + + # redirect to the login page + return redirect(url_for('auth.login')) diff --git a/flask/kontor/comics/__init__.py b/flask/kontor/comics/__init__.py new file mode 100644 index 0000000..e947d82 --- /dev/null +++ b/flask/kontor/comics/__init__.py @@ -0,0 +1,3 @@ +""" +Define routing rules for comic related information +""" diff --git a/flask/kontor/comics/forms.py b/flask/kontor/comics/forms.py new file mode 100644 index 0000000..0bae45f --- /dev/null +++ b/flask/kontor/comics/forms.py @@ -0,0 +1,31 @@ +"""Define form to edit publisher, artists and comics""" +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, BooleanField, SelectField +from wtforms.validators import DataRequired +from bson import ObjectId + + +class PublisherForm(FlaskForm): + """ + Form to add and edit a Comic publisher + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class ArtistForm(FlaskForm): + """ + Form to add and edit a Comic publisher + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class ComicForm(FlaskForm): + """ + Form to add and edit Comics + """ + title = StringField('Title', validators=[DataRequired()]) + publisher = SelectField('Publisher', coerce=ObjectId) + current_order = BooleanField('Current Order') + submit = SubmitField('Submit') diff --git a/flask/kontor/comics/models.py b/flask/kontor/comics/models.py new file mode 100644 index 0000000..e39e744 --- /dev/null +++ b/flask/kontor/comics/models.py @@ -0,0 +1,51 @@ +"""This modules declares the model for Comic related information.""" + +from flask import current_app +from pymongo.write_concern import WriteConcern +from pymodm import MongoModel, fields + + +class Publisher(MongoModel): + """Class Publisher represents a publisher of a comic.""" + name = fields.CharField() + + def __str__(self): + return "Publisher({})".format(self.name) + + @property + def comics(self): + """ + Return list of comics which has reference to this publisher + :return: + """ + comics = Comic.objects.raw({'publisher': self.pk}) + current_app.logger.debug(comics) + return comics + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Artist(MongoModel): + """Class Artist represents a comic artist.""" + name = fields.CharField() + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Comic(MongoModel): + """Class Comic represents a comic.""" + title = fields.CharField() + publisher = fields.ReferenceField(Publisher) + current_order = fields.BooleanField(default=False) + completed = fields.BooleanField(default=False) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) diff --git a/flask/kontor/comics/views.py b/flask/kontor/comics/views.py new file mode 100644 index 0000000..090b032 --- /dev/null +++ b/flask/kontor/comics/views.py @@ -0,0 +1,222 @@ +""" +Define routing rules for comics, publisher and artists +""" +from flask import Blueprint, flash, redirect, render_template, url_for +from flask_login import login_required +from bson import ObjectId +from pymongo.errors import PyMongoError +from .forms import ComicForm, PublisherForm, ArtistForm +from .models import Comic, Publisher, Artist + + +COMIC = Blueprint('comic', __name__) + + +@COMIC.route('/artists') +@login_required +def list_artists(): + """ + List all artists + :return: + """ + artists = Artist.objects.all() + return render_template('comics/artists.html', + artists=artists, title="Artists") + + +@COMIC.route('/artists/edit/', methods=['GET', 'POST']) +@login_required +def edit_artist(artist_id): + """ + Edit a comic artist + """ + artist = Artist.objects.get({'_id': ObjectId(artist_id)}) + form = ArtistForm(obj=artist) + if form.validate_on_submit(): + artist.name = form.name.data + artist.save() + flash('You have successfully edited the artist.') + return redirect(url_for('comic.list_artists')) + form.name.data = artist.name + return render_template('simpleform.html', action="Edit", + form=form, title="Edit Artist") + + +@COMIC.route('/artists/add', methods=['GET', 'POST']) +@login_required +def add_artist(): + """ + Add a artist + :return: + """ + form = ArtistForm() + if form.validate_on_submit(): + artist = Artist() + artist.name = form.name.data + try: + # add publisher to the database + artist.save() + flash('You have successfully added a new artist.') + except PyMongoError: + # in case publisher name already exists + flash('Error: artist name already exists.') + return redirect(url_for('comic.list_artists')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Artist") + + +@COMIC.route('/artists/delete/', methods=['GET', 'POST']) +@login_required +def delete_artist(artist_id): + """ + Delete a comic artist + :param artist_id: + :return: + """ + artist = Artist.objects.raw({'_id': ObjectId(artist_id)}) + if artist: + artist.delete() + flash('You have successfully deleted the comic artist.') + return redirect(url_for('comic.list_artists')) + + +@COMIC.route('/publishers') +@login_required +def list_publishers(): + """ + List all publishers + :return: + """ + publishers = Publisher.objects.all() + return render_template('comics/publishers.html', + publishers=publishers, title="Publishers") + + +@COMIC.route('/publishers/add', methods=['GET', 'POST']) +@login_required +def add_publisher(): + """ + Add a publisher to the database + :return: + """ + form = PublisherForm() + if form.validate_on_submit(): + publisher = Publisher() + publisher.name = form.name.data + try: + # add publisher to the database + publisher.save() + flash('You have successfully added a new publisher.') + except PyMongoError: + # in case publisher name already exists + flash('Error: publisher name already exists.') + return redirect(url_for('comic.list_publishers')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Publisher") + + +@COMIC.route('/publishers/edit/', methods=['GET', 'POST']) +@login_required +def edit_publisher(publisher_id): + """ + Edit a publisher + """ + publisher = Publisher.objects.get({'_id': ObjectId(publisher_id)}) + form = PublisherForm(obj=publisher) + if form.validate_on_submit(): + publisher.name = form.name.data + publisher.save() + flash('You have successfully edited the publisher.') + return redirect(url_for('comic.list_publishers')) + form.name.data = publisher.name + return render_template('simpleform.html', action="Edit", + form=form, title="Edit Publisher") + + +@COMIC.route('/publishers/delete/', methods=['GET', 'POST']) +@login_required +def delete_publisher(publisher_id): + """ + Delete a publisher + :param publisher_id: ObjectId of publisher + :return: + """ + publisher = Publisher.objects.raw({'_id': ObjectId(publisher_id)}) + if publisher: + publisher.delete() + flash('You have successfully deleted the publisher.') + return redirect(url_for('comic.list_publishers')) + + +@COMIC.route('/comics') +@login_required +def list_comics(): + """ + List all comics + :return: + """ + comics = Comic.objects.all() + return render_template('comics/comics.html', + comics=comics, title="Comics") + + +@COMIC.route('/comics/edit/', methods=['GET', 'POST']) +@login_required +def edit_comic(comic_id): + """ + Edit a comic + """ + comic = Comic.objects.get({'_id': ObjectId(comic_id)}) + form = ComicForm(obj=comic) + form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] + form.publisher.default = comic.publisher.pk + form.publisher.process_data(comic.publisher.pk) + if form.validate_on_submit(): + comic.title = form.title.data + comic.current_order = form.current_order.data + comic.publisher = form.publisher.data + comic.save() + flash('You have successfully edited the comic.') + return redirect(url_for('comic.list_comics')) + form.title.data = comic.title + form.current_order.data = comic.current_order + return render_template('simpleform.html', action="Edit", + form=form, title="Edit Comic") + + +@COMIC.route('/comics/add', methods=['GET', 'POST']) +@login_required +def add_comic(): + """ + Add a comic + :return: + """ + form = ComicForm() + form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] + if form.validate_on_submit(): + comic = Comic() + comic.title = form.title.data + comic.publisher = form.publisher.data + try: + comic.save() + flash('You have successfully added a new comic.') + except PyMongoError: + flash('Error: comic title already exists.') + return redirect(url_for('comic.list_comics')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Comic") + + +@COMIC.route('/comics/delete/', methods=['GET', 'POST']) +@login_required +def delete_comic(comic_id): + """ + Delete a comic + :param comic_id: + :return: + """ + comic = Comic.objects.raw({'_id': ObjectId(comic_id)}) + if comic: + comic.delete() + flash('You have successfully deleted the comic.') + return redirect(url_for('comic.list_comics')) diff --git a/flask/kontor/home/__init__.py b/flask/kontor/home/__init__.py new file mode 100644 index 0000000..d4468a9 --- /dev/null +++ b/flask/kontor/home/__init__.py @@ -0,0 +1,3 @@ +""" +Module for the Kontor homepage. +""" diff --git a/flask/kontor/home/views.py b/flask/kontor/home/views.py new file mode 100644 index 0000000..4e4d539 --- /dev/null +++ b/flask/kontor/home/views.py @@ -0,0 +1,39 @@ +""" +Define routing rules for homepage and dashboards +""" + +from flask import Blueprint, render_template, abort +from flask_login import login_required, current_user + +HOME = Blueprint('home', __name__) + + +@HOME.route('/') +def homepage(): + """ + Render the homepage template on the / route + """ + return render_template('home/index.html', title="Welcome") + + +@HOME.route('/dashboard') +@login_required +def dashboard(): + """ + Render the dashboard template on the /dashboard route + """ + return render_template('home/dashboard.html', title="Dashboard") + + +@HOME.route('/admin/dashboard') +@login_required +def admin_dashboard(): + """ + Render the admin_dashboard template on the /admin/dashboard route + :return: + """ + # prevent non-admins from accessing the page + if not current_user.is_admin: + abort(403) + + return render_template('home/admin_dashboard.html', title="Dashboard") diff --git a/flask/kontor/library/__init__.py b/flask/kontor/library/__init__.py new file mode 100644 index 0000000..31eb0bc --- /dev/null +++ b/flask/kontor/library/__init__.py @@ -0,0 +1,3 @@ +""" +Define routing rules for library related information +""" diff --git a/flask/kontor/library/forms.py b/flask/kontor/library/forms.py new file mode 100644 index 0000000..a080781 --- /dev/null +++ b/flask/kontor/library/forms.py @@ -0,0 +1,35 @@ +""" +Define form to edit publisher, artists and books +""" +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, SelectField, IntegerField +from wtforms.validators import DataRequired, Optional, NumberRange +from bson import ObjectId + + +class PublisherForm(FlaskForm): + """ + Form to add and edit a Comic publisher + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class AuthorForm(FlaskForm): + """ + Form to add and edit a Comic publisher + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class BookForm(FlaskForm): + """ + Form to add and edit Comics + """ + title = StringField('Title', validators=[DataRequired()]) + author = SelectField('Author', coerce=ObjectId) + publisher = SelectField('Publisher', coerce=ObjectId) + isbn = StringField('ISBN', validators=[Optional()]) + year = IntegerField('Year', validators=[NumberRange(min=1970, max=2050)]) + submit = SubmitField('Submit') diff --git a/flask/kontor/library/models.py b/flask/kontor/library/models.py new file mode 100644 index 0000000..7ca5c58 --- /dev/null +++ b/flask/kontor/library/models.py @@ -0,0 +1,55 @@ +""" +This module declares the model for the library related information. +""" +from pymongo.write_concern import WriteConcern +from pymodm import MongoModel, fields + + +class Publisher(MongoModel): + """ + Class Publisher represents a publisher of a book. + """ + name = fields.CharField() + + def __str__(self): + return "Publisher({})".format(self.name) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Author(MongoModel): + """ + Class Author represents an author of a book. + """ + name = fields.CharField() + + def __str__(self): + return "Author({})".format(self.name) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Book(MongoModel): + """ + Class book represents a book. + """ + title = fields.CharField() + author = fields.ReferenceField(Author) + publisher = fields.ReferenceField(Publisher) + isbn = fields.CharField(blank=True) + year = fields.IntegerField(blank=True) + edition = fields.CharField() + + def __str__(self): + return "Book({})".format(self.title) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) diff --git a/flask/kontor/library/views.py b/flask/kontor/library/views.py new file mode 100644 index 0000000..1b53eee --- /dev/null +++ b/flask/kontor/library/views.py @@ -0,0 +1,228 @@ +"""Define routing rules for book publishers""" +from flask import Blueprint, flash, redirect, render_template, url_for +from flask_login import login_required +from bson import ObjectId +from pymongo.errors import PyMongoError +from .forms import PublisherForm, AuthorForm, BookForm +from .models import Publisher, Author, Book + + +LIBRARY = Blueprint('library', __name__) + + +@LIBRARY.route('/publishers') +@login_required +def list_publishers(): + """ + List all publishers + :return: + """ + publishers = Publisher.objects.all() + return render_template('library/publishers.html', + publishers=publishers, title="Publishers") + + +@LIBRARY.route('/publishers/add', methods=['GET', 'POST']) +@login_required +def add_publisher(): + """ + Add a publisher to the database + :return: + """ + form = PublisherForm() + if form.validate_on_submit(): + publisher = Publisher() + publisher.name = form.name.data + try: + publisher.save() + flash('You have successfully added a new publisher.') + except PyMongoError: + flash('Error: publisher name already exists.') + return redirect(url_for('library.list_publishers')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Publisher") + + +@LIBRARY.route('/publishers/edit/', methods=['GET', 'POST']) +@login_required +def edit_publisher(publisher_id): + """ + Edit a publisher + """ + publisher = Publisher.objects.get({'_id': ObjectId(publisher_id)}) + form = PublisherForm(obj=publisher) + if form.validate_on_submit(): + publisher.name = form.name.data + publisher.save() + flash('You have successfully edited the publisher.') + return redirect(url_for('library.list_publishers')) + form.name.data = publisher.name + return render_template('simpleform.html', action="Edit", + form=form, publisher=publisher, title="Edit Publisher") + + +@LIBRARY.route('/publishers/delete/', methods=['GET', 'POST']) +@login_required +def delete_publisher(publisher_id): + """ + Delete a publisher + :param publisher_id: ObjectId of publisher + :return: + """ + publisher = Publisher.objects.raw({'_id': ObjectId(publisher_id)}) + if publisher: + publisher.delete() + flash('You have successfully deleted the publisher.') + return redirect(url_for('library.list_publishers')) + + +@LIBRARY.route('/authors') +@login_required +def list_authors(): + """ + List all authors + :return: + """ + authors = Author.objects.all() + return render_template('library/authors.html', + authors=authors, title="Authors") + + +@LIBRARY.route('/authors/edit/', methods=['GET', 'POST']) +@login_required +def edit_author(author_id): + """ + Edit a book artist + """ + author = Author.objects.get({'_id': ObjectId(author_id)}) + form = AuthorForm(obj=author) + if form.validate_on_submit(): + author.name = form.name.data + author.save() + flash('You have successfully edited the author.') + return redirect(url_for('library.list_authors')) + form.name.data = author.name + return render_template('simpleform.html', action="Edit", + form=form, author=author, title="Edit Author") + + +@LIBRARY.route('/authors/add', methods=['GET', 'POST']) +@login_required +def add_author(): + """ + Add a author + :return: + """ + form = AuthorForm() + if form.validate_on_submit(): + author = Author() + author.name = form.name.data + try: + author.save() + flash('You have successfully added a new author.') + except PyMongoError: + flash('Error: author name already exists.') + return redirect(url_for('library.list_authors')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Author") + + +@LIBRARY.route('/authors/delete/', methods=['GET', 'POST']) +@login_required +def delete_author(author_id): + """ + Delete a author + :param author_id: + :return: + """ + author = Author.objects.raw({'_id': ObjectId(author_id)}) + if author: + author.delete() + flash('You have successfully deleted the author.') + return redirect(url_for('library.list_authors')) + + +@LIBRARY.route('/books') +@login_required +def list_books(): + """ + List all comics + :return: + """ + books = Book.objects.all() + return render_template('library/books.html', + books=books, title="Books") + + +@LIBRARY.route('/books/edit/', methods=['GET', 'POST']) +@login_required +def edit_book(book_id): + """ + Edit a book + """ + book = Book.objects.get({'_id': ObjectId(book_id)}) + form = BookForm(obj=book) + form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] + if book.publisher: + form.publisher.default = book.publisher.pk + form.publisher.process_data(book.publisher.pk) + form.author.choices = [(a.pk, a.name) for a in Author.objects.all()] + if book.author: + form.author.default = book.author.pk + form.author.process_data(book.author.pk) + if form.validate_on_submit(): + book.title = form.title.data + book.publisher = form.publisher.data + book.author = form.author.data + if form.isbn.data: + book.isbn = form.isbn.data + else: + book.isbn = None + book.year = form.year.data + book.save() + flash('You have successfully edited the book.') + return redirect(url_for('library.list_books')) + form.title.data = book.title + return render_template('simpleform.html', action="Edit", + form=form, book=book, title="Edit Book") + + +@LIBRARY.route('/books/add', methods=['GET', 'POST']) +@login_required +def add_book(): + """ + Add a book + :return: + """ + form = BookForm() + form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] + form.author.choices = [(a.pk, a.name) for a in Author.objects.all()] + if form.validate_on_submit(): + book = Book() + book.title = form.title.data + book.publisher = form.publisher.data + book.author = form.author.data + book.year = form.year.data + try: + book.save() + flash('You have successfully added a new book.') + except PyMongoError: + flash('Error: book title already exists.') + return redirect(url_for('library.list_books')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Book") + + +@LIBRARY.route('/books/delete/', methods=['GET', 'POST']) +@login_required +def delete_book(book_id): + """ + Delete a book + :param book_id: + :return: + """ + book = Book.objects.raw({'_id': ObjectId(book_id)}) + if book: + book.delete() + flash('You have successfully deleted the book.') + return redirect(url_for('library.list_books')) diff --git a/flask/kontor/models.py b/flask/kontor/models.py new file mode 100644 index 0000000..7150364 --- /dev/null +++ b/flask/kontor/models.py @@ -0,0 +1,66 @@ +""" +This modules declares the model for Kontor users. +""" +from pymodm import MongoModel, fields +from pymongo.write_concern import WriteConcern +from flask_login import UserMixin +from werkzeug.security import generate_password_hash, check_password_hash +from kontor import _LOGIN_MANAGER_ + + +class User(UserMixin, MongoModel): + """ + This class represents an user for the Kontor application. + """ + email = fields.EmailField() + username = fields.CharField(max_length=60) + first_name = fields.CharField(max_length=60) + last_name = fields.CharField(max_length=60) + password_hash = fields.CharField(max_length=128) + is_admin = fields.BooleanField(default=False) + + @property + def password(self): + """ + Prevent pasword from being accessed + """ + raise AttributeError('password is not a readable attribute.') + + @password.setter + def password(self, password): + """ + Set password to a hashed password + """ + self.password_hash = generate_password_hash(password) + + def verify_password(self, password): + """ + Check if hashed password matches actual password + """ + return check_password_hash(self.password_hash, password) + + def get_id(self): + """ + Get username as id + :return: + """ + return self.username + + def __repr__(self): + return ''.format(self.username) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +# Set up user_loader +@_LOGIN_MANAGER_.user_loader +def load_user(user_name): + """ + Get list of users from database + :param user_name: + :return: + """ + return User.objects.get({'username': user_name}) diff --git a/flask/kontor/office/__init__.py b/flask/kontor/office/__init__.py new file mode 100644 index 0000000..d830d41 --- /dev/null +++ b/flask/kontor/office/__init__.py @@ -0,0 +1,3 @@ +""" +Define routing rules for office related information +""" diff --git a/flask/kontor/static/css/style.css b/flask/kontor/static/css/style.css new file mode 100644 index 0000000..35fe053 --- /dev/null +++ b/flask/kontor/static/css/style.css @@ -0,0 +1,126 @@ +/* app/static/css/style.css */ + +body, html { + width: 100%; + height: 100%; +} + +body, h1, h2, h3 { + font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 700; +} + +a, .navbar-default .navbar-brand, .navbar-default .navbar-nav>li>a { + color: #aec251; +} + +a:hover, .navbar-default .navbar-brand:hover, .navbar-default .navbar-nav>li>a:hover { + color: #687430; +} + +footer { + padding: 50px 0; + background-color: #f8f8f8; +} + +p.copyright { + margin: 15px 0 0; +} + +.alert-info { + width: 50%; + margin: auto; + color: #687430; + background-color: #e6ecca; + border-color: #aec251; +} + +.btn-default { + border-color: #aec251; + color: #aec251; +} + +.btn-default:hover { + background-color: #aec251; +} + +.center { + margin: auto; + width: 70%; + padding: 10px; +} + +.content-section { + padding: 50px 0; + border-top: 1px solid #e7e7e7; +} + +.footer, .push { + clear: both; + height: 4em; +} + +.intro-divider { + width: 400px; + border-top: 1px solid #f8f8f8; + border-bottom: 1px solid rgba(0,0,0,0.2); +} + +.intro-header { + padding-top: 50px; + padding-bottom: 50px; + text-align: center; + color: #f8f8f8; + background: url(../img/intro-bg.jpg) no-repeat center center; + background-size: cover; + height: 100%; +} + +.intro-message { + position: relative; + padding-top: 20%; + padding-bottom: 20%; +} + +.intro-message > h1 { + margin: 0; + text-shadow: 2px 2px 3px rgba(0,0,0,0.6); + font-size: 5em; +} + +.intro-message > h3 { + text-shadow: 2px 2px 3px rgba(0,0,0,0.6); +} + +.lead { + font-size: 18px; + font-weight: 400; +} + +.topnav { + font-size: 14px; +} + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -4em; +} + +.outer { + display: table; + position: absolute; + height: 70%; + width: 100%; +} + +.middle { + display: table-cell; + vertical-align: middle; +} + +.inner { + margin-left: auto; + margin-right: auto; +} diff --git a/flask/kontor/templates/admin/users/user.html b/flask/kontor/templates/admin/users/user.html new file mode 100644 index 0000000..50df2c4 --- /dev/null +++ b/flask/kontor/templates/admin/users/user.html @@ -0,0 +1,21 @@ + + +{% import "bootstrap/wtf.html" as wtf %} +{% extends "base.html" %} +{% block title %}Edit User{% endblock %} +{% block body %} +
+
+
+
+
+

Edit User

+
+
+ {{ wtf.quick_form(form) }} +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/admin/users/users.html b/flask/kontor/templates/admin/users/users.html new file mode 100644 index 0000000..b23f67c --- /dev/null +++ b/flask/kontor/templates/admin/users/users.html @@ -0,0 +1,58 @@ + + +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Users{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Users

+ {% if users %} +
+
+ + + + + + + + + + + {% for user in users %} + {% if user.is_admin %} + + + + + + + {% else %} + + + + + + + {% endif %} + {% endfor %} + +
Name Email Username Edit
Admin N/A N/A N/A
{{ user.first_name }} {{ user.last_name }} {{ user.email }} {{ user.username }} + + Edit + +
+
+ {% endif %} +
+
+
+
+ +{% endblock %} diff --git a/flask/kontor/templates/auth/login.html b/flask/kontor/templates/auth/login.html new file mode 100644 index 0000000..8b70e01 --- /dev/null +++ b/flask/kontor/templates/auth/login.html @@ -0,0 +1,18 @@ + + +{% import "bootstrap/utils.html" as utils %} +{% import "bootstrap/wtf.html" as wtf %} +{% extends "base.html" %} +{% block title %}Login{% endblock %} +{% block body %} +
+
+ {{ utils.flashed_messages() }} +
+
+

Login to your account

+
+ {{ wtf.quick_form(form) }} +
+
+{% endblock %} diff --git a/flask/kontor/templates/auth/register.html b/flask/kontor/templates/auth/register.html new file mode 100644 index 0000000..55d4918 --- /dev/null +++ b/flask/kontor/templates/auth/register.html @@ -0,0 +1,14 @@ + + +{% import "bootstrap/wtf.html" as wtf %} +{% extends "base.html" %} +{% block title %}Register{% endblock %} +{% block body %} +
+
+

Register for an account

+
+ {{ wtf.quick_form(form) }} +
+
+{% endblock %} diff --git a/flask/kontor/templates/base.html b/flask/kontor/templates/base.html new file mode 100644 index 0000000..80bdc27 --- /dev/null +++ b/flask/kontor/templates/base.html @@ -0,0 +1,107 @@ + + + + + {{ title }} | Kontor + + + + + + + +
+
+ {% block body %} + {% endblock %} +
+
+
+
+
+

+

+ +
+
+ +
+ + diff --git a/flask/kontor/templates/comics/artists.html b/flask/kontor/templates/comics/artists.html new file mode 100644 index 0000000..a92415e --- /dev/null +++ b/flask/kontor/templates/comics/artists.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Comic Artists{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Comic Artists

+ {% if artists %} +
+
+ + + + + + + + + + {% for artist in artists %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ artist.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No artists have been added.

+
+ {% endif %} + + + Add Artist + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/comics/comics.html b/flask/kontor/templates/comics/comics.html new file mode 100644 index 0000000..c596ad5 --- /dev/null +++ b/flask/kontor/templates/comics/comics.html @@ -0,0 +1,66 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Comics{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Comics

+ {% if comics %} +
+
+ + + + + + + + + + + + {% for comic in comics %} + + + + + + + + {% endfor %} + +
Title Publisher Current Order Edit Delete
{{ comic.title }} + + {{ comic.publisher.name }} + + {{ comic.current_order }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No comics have been added.

+
+ {% endif %} + + + Add Comic + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/comics/publishers.html b/flask/kontor/templates/comics/publishers.html new file mode 100644 index 0000000..aa09f7b --- /dev/null +++ b/flask/kontor/templates/comics/publishers.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Comic Publishers{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Comic Publishers

+ {% if publishers %} +
+
+ + + + + + + + + + {% for publisher in publishers %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ publisher.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No comics have been added.

+
+ {% endif %} + + + Add Publisher + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/home/admin_dashboard.html b/flask/kontor/templates/home/admin_dashboard.html new file mode 100644 index 0000000..77a0c99 --- /dev/null +++ b/flask/kontor/templates/home/admin_dashboard.html @@ -0,0 +1,31 @@ + + +{% extends "base.html" %} +{% block title %}Admin Dashboard{% endblock %} +{% block body %} +
+
+
+
+
+

Admin Dashboard

+
+ +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/home/dashboard.html b/flask/kontor/templates/home/dashboard.html new file mode 100644 index 0000000..6764375 --- /dev/null +++ b/flask/kontor/templates/home/dashboard.html @@ -0,0 +1,20 @@ + + +{% extends "base.html" %} +{% block title %}Dashboard{% endblock %} +{% block body %} +
+
+
+
+
+

The Dashboard

+

We made it here!

+
+ +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/home/index.html b/flask/kontor/templates/home/index.html new file mode 100644 index 0000000..5b01225 --- /dev/null +++ b/flask/kontor/templates/home/index.html @@ -0,0 +1,20 @@ + + +{% extends "base.html" %} +{% block title %}Home{% endblock %} +{% block body %} +
+
+
+
+
+

Project Kontor

+

Store everything!

+
+ +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/library/authors.html b/flask/kontor/templates/library/authors.html new file mode 100644 index 0000000..8c24f7b --- /dev/null +++ b/flask/kontor/templates/library/authors.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Library Authors{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Library Authors

+ {% if authors %} +
+
+ + + + + + + + + + {% for author in authors %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ author.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No artists have been added.

+
+ {% endif %} + + + Add Author + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/library/books.html b/flask/kontor/templates/library/books.html new file mode 100644 index 0000000..6a6ef76 --- /dev/null +++ b/flask/kontor/templates/library/books.html @@ -0,0 +1,74 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Books{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Books

+ {% if books %} +
+
+ + + + + + + + + + + + + + {% for book in books %} + + + + + + + + + + {% endfor %} + +
Title Author Publisher ISBN Year Edit Delete
{{ book.title }} + + {{ book.author.name }} + + + + {{ book.publisher.name }} + + {{ book.isbn }} {{ book.year }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No books have been added.

+
+ {% endif %} + + + Add Book + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/library/publishers.html b/flask/kontor/templates/library/publishers.html new file mode 100644 index 0000000..d0d363c --- /dev/null +++ b/flask/kontor/templates/library/publishers.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Book Publishers{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Book Publishers

+ {% if publishers %} +
+
+ + + + + + + + + + {% for publisher in publishers %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ publisher.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No publishers have been added.

+
+ {% endif %} + + + Add Publisher + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/simpleform.html b/flask/kontor/templates/simpleform.html new file mode 100644 index 0000000..6ce9d3b --- /dev/null +++ b/flask/kontor/templates/simpleform.html @@ -0,0 +1,21 @@ +{% import "bootstrap/wtf.html" as wtf %} +{% extends "base.html" %} +{% block title %}{{ title }}{% endblock %} +{% block body %} +
+
+
+
+
+

{{ title }}

+
+
+ {{ wtf.quick_form(form) }} +
+
+
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/tysc/manufacturers.html b/flask/kontor/templates/tysc/manufacturers.html new file mode 100644 index 0000000..f71ed75 --- /dev/null +++ b/flask/kontor/templates/tysc/manufacturers.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}{{ title }}{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

{{ title }}

+ {% if manufacturers %} +
+
+ + + + + + + + + + {% for manufacturer in manufacturers %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ manufacturer.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No manufacturers have been added.

+
+ {% endif %} + + + Add Manufacturer + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/tysc/players.html b/flask/kontor/templates/tysc/players.html new file mode 100644 index 0000000..db25067 --- /dev/null +++ b/flask/kontor/templates/tysc/players.html @@ -0,0 +1,60 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}{{ title }}{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

{{ title }}

+ {% if players %} +
+
+ + + + + + + + + + + {% for player in players %} + + + + + + + {% endfor %} + +
First Name Last Name Edit Delete
{{ player.first_name }} {{ player.last_name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No players have been added.

+
+ {% endif %} + + + Add Player + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/tysc/positions.html b/flask/kontor/templates/tysc/positions.html new file mode 100644 index 0000000..3b49e6e --- /dev/null +++ b/flask/kontor/templates/tysc/positions.html @@ -0,0 +1,62 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}{{ title }}{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

{{ title }}

+ {% if positions %} +
+
+ + + + + + + + + + + + {% for position in positions %} + + + + + + + + {% endfor %} + +
Name Description Sport Edit Delete
{{ position.name }} {{ position.description }} {{ position.sport.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No positions have been added.

+
+ {% endif %} + + + Add Position + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/tysc/sports.html b/flask/kontor/templates/tysc/sports.html new file mode 100644 index 0000000..1efe09a --- /dev/null +++ b/flask/kontor/templates/tysc/sports.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Sports{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Sports

+ {% if sports %} +
+
+ + + + + + + + + + {% for sport in sports %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ sport.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No sports have been added.

+
+ {% endif %} + + + Add Sport + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/tysc/teams.html b/flask/kontor/templates/tysc/teams.html new file mode 100644 index 0000000..c51e9bf --- /dev/null +++ b/flask/kontor/templates/tysc/teams.html @@ -0,0 +1,60 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Teams{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Teams

+ {% if teams %} +
+
+ + + + + + + + + + + {% for team in teams %} + + + + + + + {% endfor %} + +
Name Shortname Edit Delete
{{ team.name }} {{ team.shortname }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No teams have been added.

+
+ {% endif %} + + + Add Team + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/tysc/__init__.py b/flask/kontor/tysc/__init__.py new file mode 100644 index 0000000..9f06f83 --- /dev/null +++ b/flask/kontor/tysc/__init__.py @@ -0,0 +1,377 @@ +""" +Define routing rules for TradeYourSportsCards related information +""" +from .models import Sport, Team, Position, Player +from .models import Manufacturer, CardSet, ParallelSet, InsertSet, Card + + +def initialize_model(): + """ + Initialize collections for module TradeYourSportsCards. + """ + initialize_sport() + initialize_position() + initialize_teams() + initialize_players() + initialize_manufacturers() + initialize_card_sets() + initialize_parallel_sets() + initialize_insert_sets() + initialize_cards() + + +def initialize_sport(): + """ + Initialize collection Sport with american sports. + """ + for sport in Sport.objects.all(): + sport.delete() + sports = ["Baseball", "Basketball", "Football", "Hockey"] + for sport_name in sports: + sport = Sport() + sport.name = sport_name + sport.save() + + +def initialize_position(): + """ + Initialize collection Position with data. + """ + for position in Position.objects.all(): + position.delete() + positions = { + 'Quarterback': {'short': 'QB', 'sport': 'Football'}, + 'Wide Receiver': {'short': 'WR', 'sport': 'Football'}, + 'Runningback': {'short': 'RB', 'sport': 'Football'}, + 'Linebacker': {'short': 'LB', 'sport': 'Football'}, + 'Tight End': {'short': 'TE', 'sport': 'Football'}, + 'Fullback': {'short': 'FB', 'sport': 'Football'}, + 'Strong Safety': {'short': 'SS', 'sport': 'Football'}, + 'Defensive End': {'short': 'DE', 'sport': 'Football'}, + 'Kicker': {'short': 'K', 'sport': 'Football'}, + 'Punter': {'short': 'P', 'sport': 'Football'}, + 'Left Guard': {'short': 'LG', 'sport': 'Football'}, + 'Right Guard': {'short': 'RG', 'sport': 'Football'}, + 'Offensive Tackle': {'short': 'OT', 'sport': 'Football'}, + 'Defensive Back': {'short': 'DB', 'sport': 'Football'}, + 'Cornerback': {'short': 'CB', 'sport': 'Football'}, + 'Catcher': {'short': 'C', 'sport': 'Baseball'}, + 'First Base': {'short': '1B', 'sport': 'Baseball'}, + 'Second Base': {'short': '2B', 'sport': 'Baseball'}, + 'Third Base': {'short': '3B', 'sport': 'Baseball'}, + 'Shortstop': {'short': 'SS', 'sport': 'Baseball'}, + 'Left Field': {'short': 'LF', 'sport': 'Baseball'}, + 'Center Field': {'short': 'CF', 'sport': 'Baseball'}, + 'Right Field': {'short': 'RF', 'sport': 'Baseball'}, + 'Designated Hitter': {'short': 'DH', 'sport': 'Baseball'}, + 'Pitcher': {'short': 'P', 'sport': 'Baseball'} + } + for key in positions: + sport = Sport.objects.get({'name': positions.get(key)['sport']}) + position = Position() + position.description = key + position.name = positions.get(key)['short'] + position.sport = sport + position.save() + + +def initialize_teams(): + """ + Initialize collection Team with data. + """ + for team in Team.objects.all(): + team.delete() + teams = {'Buffalo Bills': {'short': 'Bills', 'sport': 'Football'}, + 'Indianapolis Colts': {'short': 'Colts', 'sport': 'Football'}, + 'Miami Dolphins': {'short': 'Dolphins', 'sport': 'Football'}, + 'New England Patriots': {'short': 'Patriots', 'sport': 'Football'}, + 'New York Jets': {'short': 'Jets', 'sport': 'Football'}, + 'Baltimore Ravens': {'short': 'Ravens', 'sport': 'Football'}, + 'Cincinnati Bengals': {'short': 'Bengals', 'sport': 'Football'}, + 'Cleveland Browns': {'short': 'Browns', 'sport': 'Football'}, + 'Jacksonville Jaguars': {'short': 'Jaguars', 'sport': 'Football'}, + 'Pittsburgh Steelers': {'short': 'Steelers', 'sport': 'Football'}, + 'Tennessee Titans': {'short': 'Titans', 'sport': 'Football'}, + 'Denver Broncos': {'short': 'Broncos', 'sport': 'Football'}, + 'Kansas City Chiefs': {'short': 'Chiefs', 'sport': 'Football'}, + 'Oakland Raiders': {'short': 'Raiders', 'sport': 'Football'}, + 'San Diego Chargers': {'short': 'Chargers', 'sport': 'Football'}, + 'Seattle Seahawks': {'short': 'Seahawks', 'sport': 'Football'}, + 'Arizona Cardinals': {'short': 'Cardinals', 'sport': 'Football'}, + 'Dallas Cowboys': {'short': 'Cowboys', 'sport': 'Football'}, + 'New York Giants': {'short': 'Giants', 'sport': 'Football'}, + 'Philadelphia Eagles': {'short': 'Eagles', 'sport': 'Football'}, + 'Washington Redskins': {'short': 'Redskins', 'sport': 'Football'}, + 'Chicago Bears': {'short': 'Bears', 'sport': 'Football'}, + 'Detroit Lions': {'short': 'Lions', 'sport': 'Football'}, + 'Green Bay Packers': {'short': 'Packers', 'sport': 'Football'}, + 'Minnesota Vikings': {'short': 'Vikings', 'sport': 'Football'}, + 'Tampa Bay Buccaneers': {'short': 'Buccaneers', 'sport': 'Football'}, + 'Atlanta Falcons': {'short': 'Falcons', 'sport': 'Football'}, + 'Carolina Panthers': {'short': 'Panthers', 'sport': 'Football'}, + 'New Orleans Saints': {'short': 'Saints', 'sport': 'Football'}, + 'St.Louis Rams': {'short': 'Rams', 'sport': 'Football'}, + 'San Francisco 49ers': {'short': '49ers', 'sport': 'Football'}, + 'Baltimore Orioles': {'short': 'Orioles', 'sport': 'Baseball'}, + 'Boston Red Sox': {'short': 'Red Sox', 'sport': 'Baseball'}, + 'New York Yankees': {'short': 'Yankees', 'sport': 'Baseball'}, + 'Tampa Bay Devil Rays': {'short': 'Devil Rays', 'sport': 'Baseball'}, + 'Toronto Blue Jays': {'short': 'Blue Jays', 'sport': 'Baseball'}, + 'Chicago White Sox': {'short': 'White Sox', 'sport': 'Baseball'}, + 'Cleveland Indians': {'short': 'Indians', 'sport': 'Baseball'}, + 'Detroit Tigers': {'short': 'Tigers', 'sport': 'Baseball'}, + 'Kansas City Royals': {'short': 'Royals', 'sport': 'Baseball'}, + 'Minnesota Twins': {'short': 'Twins', 'sport': 'Baseball'}, + 'Anaheim Angels': {'short': 'Angels', 'sport': 'Baseball'}, + 'Oakland Athletics': {'short': 'Athletics', 'sport': 'Baseball'}, + 'Seattle Mariners': {'short': 'Mariners', 'sport': 'Baseball'}, + 'Texas Rangers': {'short': 'Rangers', 'sport': 'Baseball'}, + 'Atlanta Braves': {'short': 'Braves', 'sport': 'Baseball'}, + 'Florida Marlins': {'short': 'Marlins', 'sport': 'Baseball'}, + 'Montreal Expos': {'short': 'Expos', 'sport': 'Baseball'}, + 'New York Mets': {'short': 'Mets', 'sport': 'Baseball'}, + 'Philadelphia Phillies': {'short': 'Phillies', 'sport': 'Baseball'}, + 'Chicago Cubs': {'short': 'Cubs', 'sport': 'Baseball'}, + 'Cincinnati Reds': {'short': 'Reds', 'sport': 'Baseball'}, + 'Houston Astros': {'short': 'Astros', 'sport': 'Baseball'}, + 'Milwaukee Brewers': {'short': 'Brewers', 'sport': 'Baseball'}, + 'Pittsburgh Pirates': {'short': 'Pirates', 'sport': 'Baseball'}, + 'St.Louis Cardinals': {'short': 'Cardinals', 'sport': 'Baseball'}, + 'Arizona Diamondbacks': {'short': 'Diamondbacks', 'sport': 'Baseball'}, + 'Colorado Rockies': {'short': 'Rockies', 'sport': 'Baseball'}, + 'Los Angeles Dodgers': {'short': 'Dodgers', 'sport': 'Baseball'}, + 'San Diego Padres': {'short': 'Padres', 'sport': 'Baseball'}, + 'San Francisco Giants': {'short': 'Giants', 'sport': 'Baseball'}, + 'Boston Celtics': {'short': 'Celtics', 'sport': 'Basketball'}, + 'Miami Heat': {'short': 'Heat', 'sport': 'Basketball'}, + 'New Jersey Nets': {'short': 'Mets', 'sport': 'Basketball'}, + 'New York Knicks': {'short': 'Knicks', 'sport': 'Basketball'}, + 'Orlando Magic': {'short': 'Magic', 'sport': 'Basketball'}, + 'Philadelphia 76ers': {'short': '76ers', 'sport': 'Basketball'}, + 'Washington Wizards': {'short': 'Wizards', 'sport': 'Basketball'}, + 'Atlanta Hawks': {'short': 'Hawks', 'sport': 'Basketball'}, + 'Charlotte Hornets': {'short': 'Hornets', 'sport': 'Basketball'}, + 'Chicago Bulls': {'short': 'Bulls', 'sport': 'Basketball'}, + 'Cleveland Cavaliers': {'short': 'Cavaliers', 'sport': 'Basketball'}, + 'Detroit Pistons': {'short': 'Pistons', 'sport': 'Basketball'}, + 'Indiana Pacers': {'short': 'Pacers', 'sport': 'Basketball'}, + 'Milwaukee Bucks': {'short': 'Bucks', 'sport': 'Basketball'}, + 'Toronto Raptors': {'short': 'Raptors', 'sport': 'Basketball'}, + 'Dallas Mavericks': {'short': 'Mavericks', 'sport': 'Basketball'}, + 'Denver Nuggets': {'short': 'Nuggets', 'sport': 'Basketball'}, + 'Houston Rockets': {'short': 'Rockets', 'sport': 'Basketball'}, + 'Minnesota Timberwolves': {'short': 'Timberwolves', 'sport': 'Basketball'}, + 'San Antonio Spurs': {'short': 'Spurs', 'sport': 'Basketball'}, + 'Utah Jazz': {'short': 'Jazz', 'sport': 'Basketball'}, + 'Vancouver Grizzlies': {'short': 'Grizzlies', 'sport': 'Basketball'}, + 'Golden State Warriors': {'short': 'Warriors', 'sport': 'Basketball'}, + 'Los Angeles Clippers': {'short': 'Clippers', 'sport': 'Basketball'}, + 'Los Angeles Lakers': {'short': 'Lakers', 'sport': 'Basketball'}, + 'Phoenix Suns': {'short': 'Suns', 'sport': 'Basketball'}, + 'Portland Trail Blazers': {'short': 'Blazers', 'sport': 'Basketball'}, + 'Sacramento Kings': {'short': 'Kings', 'sport': 'Basketball'}, + 'Seattle SuperSonics': {'short': 'SuperSonics', 'sport': 'Basketball'}, + 'Boston Bruins': {'short': 'Bruins', 'sport': 'Hockey'}, + 'Buffalo Sabres': {'short': 'Sabres', 'sport': 'Hockey'}, + 'Montreal Canadiens': {'short': 'Canadiens', 'sport': 'Hockey'}, + 'Ottawa Senators': {'short': 'Senators', 'sport': 'Hockey'}, + 'Toronto Maple Leafs': {'short': 'Maple Leafs', 'sport': 'Hockey'}, + 'New Jersey Devils': {'short': 'Devils', 'sport': 'Hockey'}, + 'New York Islander': {'short': 'Islander', 'sport': 'Hockey'}, + 'New York Rangers': {'short': 'Rangers', 'sport': 'Hockey'}, + 'Philadelphia Flyers': {'short': 'Flyers', 'sport': 'Hockey'}, + 'Pittsburgh Penguins': {'short': 'Penguins', 'sport': 'Hockey'}, + 'Atlanta Trashers': {'short': 'Trashers', 'sport': 'Hockey'}, + 'Carolina Hurricanes': {'short': 'Hurricanes', 'sport': 'Hockey'}, + 'Florida Panthers': {'short': 'Panthers', 'sport': 'Hockey'}, + 'Tampa Bay Lightnings': {'short': 'Lightnings', 'sport': 'Hockey'}, + 'Washington Capitals': {'short': 'Capitals', 'sport': 'Hockey'}, + 'Chicago Blackhawks': {'short': 'Blackhawks', 'sport': 'Hockey'}, + 'Columbo Blue Jackets': {'short': 'Blue Jackets', 'sport': 'Hockey'}, + 'Detroit Red Wings': {'short': 'Red Wings', 'sport': 'Hockey'}, + 'Nashville Predators': {'short': 'Predators', 'sport': 'Hockey'}, + 'St.Louis Blues': {'short': 'Blues', 'sport': 'Hockey'}, + 'Calgary Flames': {'short': 'Flames', 'sport': 'Hockey'}, + 'Colorado Avalanche': {'short': 'Avalanche', 'sport': 'Hockey'}, + 'Edmonton Oilers': {'short': 'Oilers', 'sport': 'Hockey'}, + 'Minnesota Wild': {'short': 'Wild', 'sport': 'Hockey'}, + 'Vancouver Canucks': {'short': 'Canucks', 'sport': 'Hockey'}, + 'Anaheim Mighty Ducks': {'short': 'Mighty Ducks', 'sport': 'Hockey'}, + 'Dallas Stars': {'short': 'Stars', 'sport': 'Hockey'}, + 'Los Angeles Kings': {'short': 'Kings', 'sport': 'Hockey'}, + 'Phoenix Coyotes': {'short': 'Coyotes', 'sport': 'Hockey'}, + 'San Jose Sharks': {'short': 'Sharks', 'sport': 'Hockey'}, + 'Houston Texans': {'short': 'Texans', 'sport': 'Football'}, + 'Houston Oilers': {'short': 'Oilers', 'sport': 'Football'} + } + for key in teams: + sport = Sport.objects.get({'name': teams.get(key)['sport']}) + team = Team() + team.name = key + team.shortname = teams.get(key)['short'] + team.sport = sport + team.save() + + +def initialize_players(): + """ + Initialize collection Manufacturer with data. + """ + for player in Player.objects.all(): + player.delete() + players = ['Pathon, Jerome', 'Bruschi, Tedy', 'Couch, Tim', 'Shea, Aaron', + 'Lewis, Jamal', 'Lewis, Jermaine', 'Banks, Tony', 'Fuamatu-Ma\'Afala, Chris', + 'Bettis, Jerome', 'Stewart, Kordell', 'Moon, Warren', 'Lockett, Kevin', + 'Gannon, Rich', 'Jett, James', 'Strong, Mack', 'Huard, Brock', + 'Watters, Ricky', 'Aikman, Troy', 'LaFleur, David', 'Brazzell, Chris', + 'Dayne, Ron', 'Brown, Na', 'Small, Torrance', 'Lewis, Chad', 'Murrell, Adrian', + 'Smith, Maurice', 'Chandler, Chris', 'Kanell, Danny', 'Williams, Ricky', + 'Garcia, Jeff', 'Streets, Tai', 'Garner, Charlie', 'Rice, Jerry', + 'Owens, Terrell', 'Bruce, Isaac', 'Canidate, Trung'] + for player_name in players: + player = Player() + (last_name, first_name) = player_name.split(',') + player.last_name = last_name.strip() + player.first_name = first_name.strip() + player.save() + + +def initialize_manufacturers(): + """ + Initialize collection Manufacturer with data. + """ + for manufacturer in Manufacturer.objects.all(): + manufacturer.delete() + manufacturers = ['Pacific', 'Fleer', 'Bowman', 'Topps', 'Donruss', + 'Score', 'Flair', 'Upper Deck'] + for manufacturer_name in manufacturers: + manufacturer = Manufacturer() + manufacturer.name = manufacturer_name + manufacturer.save() + + +def initialize_card_sets(): + """ + Initialize collection CardSet with data. + """ + for card_set in CardSet.objects.all(): + card_set.delete() + card_sets = {'Pacific': 'Pacific', + 'Fleer': 'Fleer', + 'Bowman': 'Bowman', + 'Leaf': 'Topps', + 'Ultra': 'Fleer', + 'Mystique': 'Fleer', + 'Finest Hour': 'Pacific', + 'SP': 'Upper Deck', + 'SPx': 'Upper Deck', + 'SP Authentic': 'Upper Deck', + 'Black Diamond': 'Upper Deck' + } + for set_name in card_sets: + card_set = CardSet() + card_set.name = set_name + card_set.manufacturer = Manufacturer.objects.get({'name': card_sets.get(set_name)}) + card_set.save() + + +def initialize_parallel_sets(): + """ + Initialize collection ParallelSet with data. + """ + for parallel_set in ParallelSet.objects.all(): + parallel_set.delete() + parallel_sets = {'Mystique Gold': 'Fleer', + 'Pacific Copper': 'Pacific', + 'Pacific Gold': 'Pacific' + } + for key in parallel_sets: + manufacturer = Manufacturer.objects.get({'name': parallel_sets.get(key)}) + parallel_set = ParallelSet() + parallel_set.name = key + parallel_set.manufacturer = manufacturer + parallel_set.save() + + +def initialize_insert_sets(): + """ + Initialize collection InsertSet with data. + """ + for insert_set in InsertSet.objects.all(): + insert_set.delete() + manufacturer = Manufacturer.objects.get({'name': 'Fleer'}) + insert_set = InsertSet() + insert_set.name = 'Mystique Big Buzz' + insert_set.manufacturer = manufacturer + insert_set.save() + + +def initialize_cards(): + """ + Initialize collection Card with data. + """ + for card in Card.objects.all(): + card.delete() + players = ['Pathon, Jerome', 'Bruschi, Tedy', 'Couch, Tim', 'Shea, Aaron', + 'Lewis, Jamal', 'Lewis, Jermaine', 'Banks, Tony', 'Fuamatu-Ma\'Afala, Chris', + 'Bettis, Jerome', 'Stewart, Kordell', 'Moon, Warren', 'Lockett, Kevin', + 'Gannon, Rich', 'Jett, James', 'Strong, Mack', 'Huard, Brock', + 'Watters, Ricky', 'Aikman, Troy', 'LaFleur, David', 'Brazzell, Chris', + 'Dayne, Ron', 'Brown, Na', 'Small, Torrance', 'Lewis, Chad', 'Murrell, Adrian', + 'Smith, Maurice', 'Chandler, Chris', 'Kanell, Danny', 'Williams, Ricky', + 'Garcia, Jeff', 'Streets, Tai', 'Garner, Charlie', 'Rice, Jerry', + 'Owens, Terrell', 'Bruce, Isaac', 'Canidate, Trung'] + cards = [ + [0, 'Indianapolis Colts', 'Pacific', 'Pacific', None, None, False, 2001, 185], + [1, 'Indianapolis Colts', 'Pacific', 'Pacific', None, None, False, 2001, 250], + [2, 'Cleveland Browns', 'Pacific', 'Pacific', None, None, False, 2001, 103], + [3, 'Cleveland Browns', 'Pacific', 'Pacific', None, None, False, 2001, 112], + [4, 'Baltimore Ravens', 'Pacific', 'Pacific', None, None, False, 2001, 37], + [5, 'Baltimore Ravens', 'Pacific', 'Pacific', None, None, False, 2001, 38], + [6, 'Baltimore Ravens', 'Pacific', 'Pacific', None, None, False, 2001, 31], + [7, 'Pittsburgh Steelers', 'Pacific', 'Pacific', None, None, False, 2001, 338], + [8, 'Pittsburgh Steelers', 'Pacific', 'Pacific', None, None, False, 2001, 335], + [9, 'Pittsburgh Steelers', 'Pacific', 'Pacific', None, None, False, 2001, 345], + [10, 'Kansas City Chiefs', 'Pacific', 'Pacific', None, None, False, 2001, 213], + [11, 'Kansas City Chiefs', 'Pacific', 'Pacific', None, None, False, 2001, 212], + [12, 'Oakland Raiders', 'Pacific', 'Pacific', None, None, False, 2001, 311], + [13, 'Oakland Raiders', 'Pacific', 'Pacific', None, None, False, 2001, 312], + [14, 'Seattle Seahawks', 'Pacific', 'Pacific', None, None, False, 2001, 403], + [15, 'Seattle Seahawks', 'Pacific', 'Pacific', None, None, False, 2001, 397], + [16, 'Seattle Seahawks', 'Pacific', 'Pacific', None, None, False, 2001, 404], + [17, 'Dallas Cowboys', 'Pacific', 'Pacific', None, None, False, 2001, 116], + [18, 'Dallas Cowboys', 'Pacific', 'Pacific', None, None, False, 2001, 122], + [19, 'Dallas Cowboys', 'Pacific', 'Pacific', None, None, False, 2001, 117], + [20, 'New York Giants', 'Pacific', 'Pacific', None, None, False, 2001, 281], + [21, 'New York Giants', 'Pacific', 'Pacific', None, None, False, 2001, 321], + [22, 'Philadelphia Eagles', 'Pacific', 'Pacific', None, None, False, 2001, 331], + [23, 'Philadelphia Eagles', 'Pacific', 'Pacific', None, None, False, 2001, 324], + [24, 'Washington Redskins', 'Pacific', 'Pacific', None, None, False, 2001, 445], + [25, 'Atlanta Falcons', 'Pacific', 'Pacific', None, None, False, 2001, 28], + [26, 'Atlanta Falcons', 'Pacific', 'Pacific', None, None, False, 2001, 17], + [27, 'Atlanta Falcons', 'Pacific', 'Pacific', None, None, False, 2001, 23], + [28, 'New Orleans Saints', 'Pacific', 'Pacific', None, None, False, 2001, 273], + [29, 'San Francisco 49ers', 'Pacific', 'Pacific', None, None, False, 2001, 380], + [30, 'San Francisco 49ers', 'Pacific', 'Pacific', None, None, False, 2001, 390], + [31, 'San Francisco 49ers', 'Pacific', 'Pacific', None, None, False, 2001, 381], + [32, 'San Francisco 49ers', 'Pacific', 'Pacific', None, None, False, 2001, 387], + [33, 'San Francisco 49ers', 'Pacific', 'Pacific', None, None, False, 2001, 386], + [34, 'St.Louis Rams', 'Pacific', 'Pacific', None, None, False, 2001, 349], + [35, 'St.Louis Rams', 'Pacific', 'Pacific', None, None, False, 2001, 350], + ] + for card_data in cards: + card = Card() + player_name = players[card_data[0]] + (last_name, first_name) = player_name.split(',') + card.player = Player.objects.get( + {'last_name': last_name.strip(), 'first_name': first_name.strip()} + ) + card.team = Team.objects.get({'name': card_data[1]}) + card.manufacturer = Manufacturer.objects.get({'name': card_data[2]}) + card.card_set = CardSet.objects.get({'name': card_data[3]}) + card.parallel_set = card_data[4] + card.insert_set = card_data[5] + card.rookie = card_data[6] + card.year = card_data[7] + card.number = card_data[8] + card.save() diff --git a/flask/kontor/tysc/forms.py b/flask/kontor/tysc/forms.py new file mode 100644 index 0000000..9cb5c3c --- /dev/null +++ b/flask/kontor/tysc/forms.py @@ -0,0 +1,76 @@ +""" +Define form to edit sport, teams, player, manufacturers and card types +""" +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, SelectField, IntegerField, BooleanField +from wtforms.validators import DataRequired, Optional, NumberRange +from bson import ObjectId + + +class SportForm(FlaskForm): + """ + Form to add and edit a Sport + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class TeamForm(FlaskForm): + """ + Form to add and edit a team + """ + name = StringField('Name', validators=[DataRequired()]) + shortname = StringField('Shortname', validators=[DataRequired()]) + sport = SelectField('Sport', coerce=ObjectId) + submit = SubmitField('Submit') + + +class PlayerForm(FlaskForm): + """ + Form to add and edit a player. + """ + first_name = StringField('First name', validators=[DataRequired()]) + last_name = StringField('Last name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class PositionForm(FlaskForm): + """ + Form to add and edit field positions for sports + """ + name = StringField('Name', validators=[DataRequired()]) + description = StringField('Description', validators=[DataRequired()]) + sport = SelectField('Sport', coerce=ObjectId) + submit = SubmitField('Submit') + + +class ManufacturerForm(FlaskForm): + """ + Form to add and edit a card manufacturer. + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class CardSetForm(FlaskForm): + """ + Form to add and edit a regular card set + """ + name = StringField('Name', validators=[DataRequired()]) + manufacturer = SelectField('Manufacturer', coerce=ObjectId) + submit = SubmitField('Submit') + + +class CardForm(FlaskForm): + """ + Form to add and edit a trading card + """ + player = SelectField('Player', coerce=ObjectId) + team = SelectField('Team', coerce=ObjectId) + manufacturer = SelectField('Manufacturer', coerce=ObjectId) + card_set = SelectField('Card Set', coerce=ObjectId) + parallel_set = SelectField('Parallel Set', coerce=ObjectId, validators=[Optional()]) + insert_set = SelectField('Inserts', coerce=ObjectId, validators=[Optional()]) + rookie = BooleanField('Rookie', validators=[DataRequired()]) + year = IntegerField('Year', validators=[NumberRange(min=1956, max=2020)]) + number = IntegerField('Number', validators=[DataRequired()]) diff --git a/flask/kontor/tysc/models.py b/flask/kontor/tysc/models.py new file mode 100644 index 0000000..02dc341 --- /dev/null +++ b/flask/kontor/tysc/models.py @@ -0,0 +1,155 @@ +"""This modules declares the model for TradingCards related information.""" +from pymongo.write_concern import WriteConcern +from pymodm import MongoModel, fields + + +class Sport(MongoModel): + """Class Sport represents a sport.""" + name = fields.CharField() + + def __str__(self): + """Returns printable version of Sport object.""" + return self.name + + def __repr__(self): + """Returns printable version of Sport object.""" + return "Sport({})".format(self.name) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Position(MongoModel): + """Class Position represents the position of a player for a sport.""" + name = fields.CharField(max_length=4) + description = fields.CharField(max_length=30) + sport = fields.ReferenceField(Sport) + + def __str__(self): + """Returns printable version of Position object.""" + return "{0}({1})".format(self.name, self.description) + + def __repr__(self): + """Returns printable version of Position object.""" + return "Position({0}, {1})".format(self.name, self.description) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Team(MongoModel): + """Class Team represents a team for a sport.""" + name = fields.CharField(max_length=60) + shortname = fields.CharField(max_length=30) + sport = fields.ReferenceField(Sport, blank=True) + + def __str__(self): + """Returns printable version of Team object.""" + return self.name + + def __repr__(self): + """Returns printable version of Team object.""" + return "Team({0}{1})".format(self.name, self.shortname) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Player(MongoModel): + """ + Class Player represents a player. + """ + first_name = fields.CharField(max_length=60) + last_name = fields.CharField(max_length=60) + + def __str__(self): + """Returns printable version of Team object.""" + return "{0} {1}".format(self.first_name, self.last_name) + + def __repr__(self): + """Returns printable version of Team object.""" + return "Player({0} {1})".format(self.first_name, self.last_name) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Manufacturer(MongoModel): + """ + Class Manufacturer represents a manufacturer of trading cards. + """ + name = fields.CharField() + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class CardSet(MongoModel): + """ + Class CardSet represents the regular card set. + """ + name = fields.CharField() + manufacturer = fields.ReferenceField(Manufacturer) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class ParallelSet(MongoModel): + """ + Class CardSet represents the parallel card set. + """ + name = fields.CharField() + manufacturer = fields.ReferenceField(Manufacturer) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class InsertSet(MongoModel): + """ + Class CardSet represents the inserts card set. + """ + name = fields.CharField() + manufacturer = fields.ReferenceField(Manufacturer) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Card(MongoModel): + """ + Class CardSet represents the regular card set. + """ + # pylint: disable=too-many-instance-attributes + # Nine is reasonable in this case. + player = fields.ReferenceField(Player) + team = fields.ReferenceField(Team) + manufacturer = fields.ReferenceField(Manufacturer) + card_set = fields.ReferenceField(CardSet, blank=True) + parallel_set = fields.ReferenceField(ParallelSet, blank=True) + insert_set = fields.ReferenceField(InsertSet, blank=True) + rookie = fields.BooleanField(default=False) + year = fields.IntegerField(min_value=1956, max_value=2020) + number = fields.IntegerField() + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) diff --git a/flask/kontor/tysc/views.py b/flask/kontor/tysc/views.py new file mode 100644 index 0000000..836b295 --- /dev/null +++ b/flask/kontor/tysc/views.py @@ -0,0 +1,405 @@ +""" +Define BLueprint and Views for TradeYourSportsCards +""" +from flask import Blueprint, url_for, render_template, redirect, flash +from flask_login import login_required +from bson import ObjectId +from pymongo.errors import PyMongoError +from .forms import SportForm, TeamForm, PlayerForm, PositionForm +from .forms import ManufacturerForm +from .models import Sport, Team, Player, Position +from .models import Manufacturer, CardSet, ParallelSet, InsertSet, Card + + +TYSC = Blueprint('tysc', __name__) + + +@TYSC.route('/sport') +@login_required +def list_sports(): + """ + List sports. + :return: + """ + sports = Sport.objects.all() + return render_template('tysc/sports.html', sports=sports, title="Sports") + + +@TYSC.route('/sport/edit/', methods=['GET', 'POST']) +@login_required +def edit_sport(sport_id): + """ + Edit a sport + """ + sport = Sport.objects.get({'_id': ObjectId(sport_id)}) + form = SportForm(obj=sport) + if form.validate_on_submit(): + sport.name = form.name.data + sport.save() + flash('You have successfully edited the sport.') + return redirect(url_for('tysc.list_sports')) + form.name.data = sport.name + return render_template('simpleform.html', action="Edit", + form=form, sport=sport, title="Edit Sport") + + +@TYSC.route('/sport/add', methods=['GET', 'POST']) +@login_required +def add_sport(): + """ + Add a sport + :return: + """ + form = SportForm() + if form.validate_on_submit(): + sport = Sport() + sport.name = form.name.data + try: + sport.save() + flash('You have successfully added a new sport.') + except PyMongoError: + flash('Error: sport name already exists.') + return redirect(url_for('tysc.list_sports')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Sport") + + +@TYSC.route('/sport/delete/', methods=['GET', 'POST']) +@login_required +def delete_sport(sport_id): + """ + Delete a sport + :param sport_id: + :return: + """ + sport = Sport.objects.raw({'_id': ObjectId(sport_id)}) + if sport: + sport.delete() + flash('You have successfully deleted the sport.') + return redirect(url_for('tysc.list_sports')) + + +@TYSC.route('/team') +@login_required +def list_teams(): + """ + List teams. + :return: + """ + teams = Team.objects.all() + return render_template('tysc/teams.html', teams=teams, title="Teams") + + +@TYSC.route('/team/edit/', methods=['GET', 'POST']) +@login_required +def edit_team(team_id): + """ + Edit a team + """ + team = Team.objects.get({'_id': ObjectId(team_id)}) + form = TeamForm(obj=team) + form.sport.choices = [(s.pk, s.name) for s in Sport.objects.all()] + if team.sport: + form.sport.default = team.sport.pk + form.sport.process_data(team.sport.pk) + if form.validate_on_submit(): + team.name = form.name.data + team.shortname = form.shortname.data + team.sport = form.sport.data + team.save() + flash('You have successfully edited the team.') + return redirect(url_for('tysc.list_teams')) + form.name.data = team.name + return render_template('simpleform.html', action="Edit", + form=form, team=team, title="Edit Team") + + +@TYSC.route('/team/add', methods=['GET', 'POST']) +@login_required +def add_team(): + """ + Add a team + :return: + """ + form = TeamForm() + form.sport.choices = [(s.pk, s.name) for s in Sport.objects.all()] + if form.validate_on_submit(): + team = Team() + team.name = form.name.data + team.shortname = form.shortname.data + team.sport = form.sport.data + try: + team.save() + flash('You have successfully added a new team.') + except PyMongoError: + flash('Error: team name already exists.') + return redirect(url_for('tysc.list_teams')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Team") + + +@TYSC.route('/team/delete/', methods=['GET', 'POST']) +@login_required +def delete_team(team_id): + """ + Delete a team + :param team_id: + :return: + """ + team = Team.objects.raw({'_id': ObjectId(team_id)}) + if team: + team.delete() + return redirect(url_for('tysc.list_teams')) + + +@TYSC.route('/position') +@login_required +def list_positions(): + """ + List positions. + :return: + """ + positions = Position.objects.all() + return render_template('tysc/positions.html', positions=positions, title="Positions") + + +@TYSC.route('/position/edit/', methods=['GET', 'POST']) +@login_required +def edit_position(position_id): + """ + Edit a position + """ + position = Position.objects.get({'_id': ObjectId(position_id)}) + form = PositionForm(obj=position) + form.sport.choices = [(s.pk, s.name) for s in Sport.objects.all()] + if position.sport: + form.sport.default = position.sport.pk + form.sport.process_data(position.sport.pk) + if form.validate_on_submit(): + position.name = form.name.data + position.description = form.description.data + position.sport = form.sport.data + position.save() + flash('You have successfully edited the position.') + return redirect(url_for('tysc.list_positions')) + form.name.data = position.name + form.description.data = position.description + return render_template('simpleform.html', action="Edit", + form=form, position=position, title="Edit position") + + +@TYSC.route('/position/add', methods=['GET', 'POST']) +@login_required +def add_position(): + """ + Add a position + :return: + """ + form = PositionForm() + form.sport.choices = [(s.pk, s.name) for s in Sport.objects.all()] + if form.validate_on_submit(): + position = Position() + position.name = form.name.data + position.description = form.description.data + position.sport = form.sport.data + try: + position.save() + flash('You have successfully added a new position.') + except PyMongoError: + flash('Error: team position already exists.') + return redirect(url_for('tysc.list_positions')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Position") + + +@TYSC.route('/position/delete/', methods=['GET', 'POST']) +@login_required +def delete_position(position_id): + """ + Delete a position + :param position_id: + :return: + """ + position = Position.objects.raw({'_id': ObjectId(position_id)}) + if position: + position.delete() + return redirect(url_for('tysc.list_positions')) + + +@TYSC.route('/player') +@login_required +def list_players(): + """ + List players. + :return: + """ + players = Player.objects.all() + return render_template('tysc/players.html', players=players, title="Players") + + +@TYSC.route('/player/edit/', methods=['GET', 'POST']) +@login_required +def edit_player(player_id): + """ + Edit a player + """ + player = Player.objects.get({'_id': ObjectId(player_id)}) + form = PlayerForm(obj=player) + if form.validate_on_submit(): + player.first_name = form.first_name.data + player.last_name = form.last_name.data + player.save() + flash('You have successfully edited the player.') + return redirect(url_for('tysc.list_players')) + form.first_name.data = player.first_name + form.last_name.data = player.last_name + return render_template('simpleform.html', action="Edit", + form=form, player=player, title="Edit player") + + +@TYSC.route('/player/add', methods=['GET', 'POST']) +@login_required +def add_player(): + """ + Add a player + :return: + """ + form = PlayerForm() + if form.validate_on_submit(): + player = Player() + player.first_name = form.first_name.data + player.last_name = form.last_name.data + try: + player.save() + flash('You have successfully added a new player.') + except PyMongoError: + flash('Error: player name already exists.') + return redirect(url_for('tysc.list_players')) + return render_template('simpleform.html', action="Add", + form=form, title="Add player") + + +@TYSC.route('/player/delete/', methods=['GET', 'POST']) +@login_required +def delete_player(player_id): + """ + Delete a player + :param player_id: + :return: + """ + player = Player.objects.raw({'_id': ObjectId(player_id)}) + if player: + player.delete() + return redirect(url_for('tysc.list_players')) + + +@TYSC.route('/manufacturer') +@login_required +def list_manufacturers(): + """ + List manufacturers. + :return: + """ + manufacturers = Manufacturer.objects.all() + return render_template('tysc/manufacturers.html', + manufacturers=manufacturers, + title="Manufacturers") + + +@TYSC.route('/manufacturer/edit/', methods=['GET', 'POST']) +@login_required +def edit_manufacturer(manufacturer_id): + """ + Edit a manufacturer + """ + manufacturer = Manufacturer.objects.get({'_id': ObjectId(manufacturer_id)}) + form = ManufacturerForm(obj=manufacturer) + if form.validate_on_submit(): + manufacturer.name = form.name.data + manufacturer.save() + flash('You have successfully edited the manufacturer.') + return redirect(url_for('tysc.list_manufacturers')) + form.name.data = manufacturer.name + return render_template('simpleform.html', action="Edit", + form=form, manufacturer=manufacturer, title="Edit Manufacturer") + + +@TYSC.route('/manufacturer/add', methods=['GET', 'POST']) +@login_required +def add_manufacturer(): + """ + Add a manufacturer + :return: + """ + form = ManufacturerForm() + if form.validate_on_submit(): + manufacturer = Manufacturer() + manufacturer.name = form.name.data + try: + manufacturer.save() + flash('You have successfully added a new manufacturer.') + except PyMongoError: + flash('Error: manufacturer name already exists.') + return redirect(url_for('tysc.list_manufacturers')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Manufacturer") + + +@TYSC.route('/manufacturer/delete/', methods=['GET', 'POST']) +@login_required +def delete_manufacturer(manufacturer_id): + """ + Delete a manufacturer + :param manufacturer_id: + :return: + """ + manufacturer = Manufacturer.objects.raw({'_id': ObjectId(manufacturer_id)}) + if manufacturer: + manufacturer.delete() + flash('You have successfully deleted the manufacturer.') + return redirect(url_for('tysc.list_manufacturers')) + + +@TYSC.route('/cardsets') +@login_required +def list_card_sets(): + """ + List card sets. + :return: + """ + card_sets = CardSet.objects.all() + return render_template('tysc/cardsets.html', card_sets=card_sets, title="Card Sets") + + +@TYSC.route('/parallelsets') +@login_required +def list_parallel_sets(): + """ + List card sets. + :return: + """ + parallel_sets = ParallelSet.objects.all() + return render_template('tysc/parallelsets.html', + parallel_sets=parallel_sets, title="Parallel Sets") + + +@TYSC.route('/insertsets') +@login_required +def list_insert_sets(): + """ + List card sets. + :return: + """ + insert_sets = InsertSet.objects.all() + return render_template('tysc/insertsets.html', insert_sets=insert_sets, title="Inserts") + + +@TYSC.route('/cards') +@login_required +def list_cards(): + """ + List card sets. + :return: + """ + cards = Card.objects.all() + return render_template('tysc/cards.html', cards=cards, title="Cards") diff --git a/flask/kontor/version.py b/flask/kontor/version.py new file mode 100644 index 0000000..044c225 --- /dev/null +++ b/flask/kontor/version.py @@ -0,0 +1,2 @@ +"""This module declares the version of the Kontor application.""" +__version__ = '0.0.7' diff --git a/flask/pylint.cfg b/flask/pylint.cfg new file mode 100644 index 0000000..e08fe3a --- /dev/null +++ b/flask/pylint.cfg @@ -0,0 +1,382 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +init-hook='sys.path = list(); sys.path.append("./lib/python3.4/site-packages/")' + +# Profiled execution. +profile=no + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Allow optimization of some AST trees. This will activate a peephole AST +# optimizer, which will apply various small optimizations. For instance, it can +# be used to obtain the result of joining multiple strings with the addition +# operator. Joining a lot of strings can lead to a maximum recursion error in +# Pylint and this flag can prevent that. It has one side effect, the resulting +# AST will be different than the one from reality. +optimize-ast=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=W1601,W1606,W1604,W1630,E1605,E1604,W1636,W1603,W1633,W1610,W1615,W1626,E1606,W0704,I0020,W1622,E1608,W1612,W1638,W1614,W1616,W1605,W1613,W1634,E1603,I0021,W1640,W1621,W1625,E1602,W1639,W1602,W1620,W1617,W1624,W1609,W1618,E1601,E1607,W1637,W1632,W1629,W1635,W1611,W1623,W1608,W1627,W1628,W1607,W1619,E1101,R0201,R0903,R0801,locally-disabled + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=parseable + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells whether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (RP0004). +comment=no + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis +ignored-modules= + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). +ignored-classes=SQLObject + +# When zope mode is activated, add a predefined set of Zope acquired attributes +# to generated-members. +zope=no + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E0201 when accessed. Python regular +# expressions are accepted. +generated-members=REQUEST,acl_users,aq_parent + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_$|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=100 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[BASIC] + +# Required attributes for module, separated by a comma +#required-attributes= + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=yes + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for function names +function-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for attribute names +attr-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for method names +method-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for argument names +argument-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for variable names +variable-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=__.*__ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +#ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=stringprep,optparse + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/flask/requirements.txt b/flask/requirements.txt new file mode 100644 index 0000000..f596076 --- /dev/null +++ b/flask/requirements.txt @@ -0,0 +1,11 @@ +flask +flask-login +flask-bootstrap +Flask-WTF +flask-testing +pymodm +nose +nose-htmloutput +coverage +pylint + diff --git a/flask/tests/__init__.py b/flask/tests/__init__.py new file mode 100644 index 0000000..8506c57 --- /dev/null +++ b/flask/tests/__init__.py @@ -0,0 +1,38 @@ +from flask_testing import TestCase +from kontor import create_app +from kontor.models import User + + +class TestBase(TestCase): + + def create_app(self): + + # pass in test configuration + config_name = 'testing' + app = create_app(config_name) + return app + + def setUp(self): + """ + Will be called before every test + """ + # create test admin user + admin = User() + admin.username="admin" + admin.password="admin2016" + admin.is_admin=True + admin.save() + + # create test non-admin user + employee = User() + employee.username="test_user" + employee.password="test2016" + employee.save() + + def tearDown(self): + """ + Will be called after every test + """ + users = User.objects.all() + for user in users: + user.delete() diff --git a/flask/tests/test_comics_model.py b/flask/tests/test_comics_model.py new file mode 100644 index 0000000..2a5dcc5 --- /dev/null +++ b/flask/tests/test_comics_model.py @@ -0,0 +1,51 @@ +""" +This module cantains tests for the comic data model +""" +import unittest +from . import TestBase +from kontor.comics.models import Artist, Comic, Publisher + + +class TestComicsModel(TestBase): + """This TestCase contains tests for comic data model.""" + + def setUp(self): + publisher = Publisher() + publisher.name = "Publisher1" + publisher.save() + artist = Artist() + artist.name = "Artist1" + artist.save() + comic = Comic() + comic.title = "Title1" + comic.save() + + def tearDown(self): + for comic in Comic.objects.all(): + comic.delete() + for artist in Artist.objects.all(): + artist.delete() + for publisher in Publisher.objects.all(): + publisher.delete() + + def test_comic_model(self): + comic_list = Comic.objects.all() + self.assertEqual(comic_list.count(), 1) + + def test_publisher_model(self): + self.assertEqual(Publisher.objects.all().count(), 1) + + def test_artist_model(self): + self.assertEqual(Artist.objects.all().count(), 1) + + def test_assign_publisher(self): + comic = Comic.objects.first() + publisher = Publisher.objects.first() + comic.publisher = publisher + comic.save() + self.assertEqual(publisher.name, comic.publisher.name) + self.assertEqual(publisher.comics.count(), 1) + + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_comics_view.py b/flask/tests/test_comics_view.py new file mode 100644 index 0000000..e33fd79 --- /dev/null +++ b/flask/tests/test_comics_view.py @@ -0,0 +1,44 @@ +"""This module contains tests for Comic related urls.""" +import unittest +from flask import url_for +from . import TestBase + + +class TestComicsViews(TestBase): + + def test_comics_index(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('comic.list_comics') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_comics_artist(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('comic.list_artists') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_comics_publisher(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('comic.list_publishers') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_config.txt b/flask/tests/test_config.txt new file mode 100644 index 0000000..2b847b0 --- /dev/null +++ b/flask/tests/test_config.txt @@ -0,0 +1,4 @@ +# instance/config.py + +SECRET_KEY = 'p9Bv<3Eid9%$i01' + diff --git a/flask/tests/test_kontor_model.py b/flask/tests/test_kontor_model.py new file mode 100644 index 0000000..0120475 --- /dev/null +++ b/flask/tests/test_kontor_model.py @@ -0,0 +1,14 @@ +""" +This module cantains tests for the general Kontor data model. +""" +from . import TestBase +from kontor.models import User + + +class TestKontorModel(TestBase): + """This TestCase contains tests for users.""" + def test_user_model(self): + """ + Test number of records in User collection + """ + self.assertEqual(User.objects.all().count(), 2) diff --git a/flask/tests/test_kontor_view.py b/flask/tests/test_kontor_view.py new file mode 100644 index 0000000..101ae2f --- /dev/null +++ b/flask/tests/test_kontor_view.py @@ -0,0 +1,58 @@ +"""This module contains tests for Comic related urls.""" +import unittest +from flask import url_for +from . import TestBase + + +class TestKontorViews(TestBase): + + def test_homepage_view(self): + """ + Test that homepage is accessible without login + """ + response = self.client.get(url_for('home.homepage')) + self.assertEqual(response.status_code, 200) + + def test_login_view(self): + """ + Test that login page is accessible without login + """ + response = self.client.get(url_for('auth.login')) + self.assertEqual(response.status_code, 200) + + def test_logout_view(self): + """ + Test that logout link is inaccessible without login + and redirects to login page then to logout + """ + target_url = url_for('auth.logout') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_dashboard_view(self): + """ + Test that dashboard is inaccessible without login + and redirects to login page then to dashboard + """ + target_url = url_for('home.dashboard') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_admin_dashboard_view(self): + """ + Test that dashboard is inaccessible without login + and redirects to login page then to dashboard + """ + target_url = url_for('home.admin_dashboard') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_library_model.py b/flask/tests/test_library_model.py new file mode 100644 index 0000000..635bb58 --- /dev/null +++ b/flask/tests/test_library_model.py @@ -0,0 +1,49 @@ +""" +This module cantains tests for the comic data model +""" +import unittest +from . import TestBase +from kontor.library.models import Author, Publisher, Book + + +class TestComicsModel(TestBase): + """This TestCase contains tests for comic data model.""" + + def setUp(self): + publisher = Publisher() + publisher.name = "Publisher1" + publisher.save() + author = Author() + author.name = "Autor1" + author.save() + book = Book() + book.title = "Title1" + book.save() + + def tearDown(self): + for book in Book.objects.all(): + book.delete() + for author in Author.objects.all(): + author.delete() + for publisher in Publisher.objects.all(): + publisher.delete() + + def test_comic_model(self): + book_list = Book.objects.all() + self.assertEqual(book_list.count(), 1) + + def test_publisher_model(self): + self.assertEqual(Publisher.objects.all().count(), 1) + + def test_artist_model(self): + self.assertEqual(Author.objects.all().count(), 1) + + def test_assign_publisher(self): + book = Book.objects.first() + publisher = Publisher.objects.first() + book.publisher = publisher + book.save() + self.assertEqual(publisher.name, book.publisher.name) + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_library_view.py b/flask/tests/test_library_view.py new file mode 100644 index 0000000..51b2c30 --- /dev/null +++ b/flask/tests/test_library_view.py @@ -0,0 +1,44 @@ +"""This module contains tests for Comic related urls.""" +import unittest +from flask import url_for +from . import TestBase + + +class TestLibraryViews(TestBase): + + def test_library_index(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('library.list_books') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_library_author(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('library.list_authors') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_library_publisher(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('library.list_publishers') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_tysc_model.py b/flask/tests/test_tysc_model.py new file mode 100644 index 0000000..228cc49 --- /dev/null +++ b/flask/tests/test_tysc_model.py @@ -0,0 +1,53 @@ +""" +This module cantains tests for the TYSC data model +""" +import unittest +from . import TestBase +from kontor.tysc import initialize_model +from kontor.tysc.models import Sport, Position, Team, Player +from kontor.tysc.models import Manufacturer, CardSet, ParallelSet, InsertSet, Card + + +class TestTYSCModel(TestBase): + """ + This TestCase contains tests for TYSC data model. + """ + _initialize_db_ = True + + def setUp(self): + if self._initialize_db_: + initialize_model() + self._initialize_db_ = False + + def tearDown(self): + pass + + def test_sport_model(self): + self.assertEqual(Sport.objects.all().count(), 4) + + def test_position_model(self): + self.assertEqual(Position.objects.all().count(), 25) + + def test_team_model(self): + self.assertEqual(Team.objects.all().count(), 122) + + def test_manufacturer_model(self): + self.assertEqual(Manufacturer.objects.all().count(), 8) + + def test_cardset_model(self): + self.assertEqual(CardSet.objects.all().count(), 11) + + def test_parallelset_model(self): + self.assertEqual(ParallelSet.objects.all().count(), 3) + + def test_insertset_model(self): + self.assertEqual(InsertSet.objects.all().count(), 1) + + def test_playerset_model(self): + self.assertEqual(Player.objects.all().count(), 36) + + def test_card_model(self): + self.assertEqual(Card.objects.all().count(), 36) + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_tysc_view.py b/flask/tests/test_tysc_view.py new file mode 100644 index 0000000..11fc222 --- /dev/null +++ b/flask/tests/test_tysc_view.py @@ -0,0 +1,110 @@ +"""This module contains tests for TradeYourSportsCards related urls.""" +import unittest +from flask import url_for +from . import TestBase + + +class TestTYSCViews(TestBase): + + def test_tysc_index(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_cards') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_sport(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_sports') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_team(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_teams') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_player(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_players') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_manufacturer(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_manufacturers') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_card_set(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_card_sets') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_parallel_set(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_parallel_sets') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_insert_set(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_insert_sets') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_card(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_cards') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + +if __name__ == '__main__': + unittest.main() From 5c261529fc4ec7cb15869b40a31efc7d9c48e935 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Wed, 8 Jan 2025 22:34:21 +0100 Subject: [PATCH 17/91] import from kontor-flask branch develop/0.1.0 --- flask/.gitignore | 2 + flask/README.md | 2 + flask/app.py | 5 + flask/config.py | 47 -- flask/docs/Makefile | 20 - flask/docs/conf.py | 200 --------- flask/docs/index.rst | 20 - flask/kontor/__init__.py | 115 +++-- flask/kontor/admin/__init__.py | 4 - flask/kontor/admin/forms.py | 17 - flask/kontor/admin/views.py | 63 --- flask/kontor/api/__init__.py | 5 + flask/kontor/api/routes.py | 30 ++ flask/kontor/auth/__init__.py | 26 +- flask/kontor/auth/forms.py | 50 --- flask/kontor/auth/models.py | 45 ++ flask/kontor/auth/views.py | 79 ---- flask/kontor/comics/__init__.py | 12 +- flask/kontor/comics/api.py | 28 ++ flask/kontor/comics/forms.py | 31 -- flask/kontor/comics/models.py | 51 --- flask/kontor/comics/routes.py | 16 + .../kontor/comics/templates/comics/list.html | 28 ++ .../kontor/comics/templates/comics/view.html | 27 ++ flask/kontor/comics/views.py | 222 ---------- flask/kontor/config.py | 10 + flask/kontor/extensions.py | 5 + flask/kontor/home/__init__.py | 3 - flask/kontor/home/views.py | 39 -- flask/kontor/library/__init__.py | 3 - flask/kontor/library/forms.py | 35 -- flask/kontor/library/models.py | 55 --- flask/kontor/library/views.py | 228 ---------- flask/kontor/main/__init__.py | 5 + flask/kontor/main/routes.py | 18 + flask/kontor/media/__init__.py | 9 + flask/kontor/media/api.py | 18 + flask/kontor/media/models.py | 43 ++ flask/kontor/media/routes.py | 16 + .../templates/media/mediafile_detail.html | 29 ++ .../media/templates/media/mediafile_list.html | 30 ++ flask/kontor/models.py | 101 ++--- flask/kontor/office/__init__.py | 3 - flask/kontor/static/css/style.css | 126 ------ flask/kontor/templates/admin/users/user.html | 21 - flask/kontor/templates/admin/users/users.html | 58 --- flask/kontor/templates/auth/login.html | 18 - flask/kontor/templates/auth/register.html | 14 - flask/kontor/templates/base.html | 157 +++---- flask/kontor/templates/comics/artists.html | 58 --- flask/kontor/templates/comics/comics.html | 66 --- flask/kontor/templates/comics/publishers.html | 58 --- .../templates/home/admin_dashboard.html | 31 -- flask/kontor/templates/home/dashboard.html | 20 - flask/kontor/templates/home/index.html | 20 - flask/kontor/templates/index.html | 8 + flask/kontor/templates/library/authors.html | 58 --- flask/kontor/templates/library/books.html | 74 ---- .../kontor/templates/library/publishers.html | 58 --- flask/kontor/templates/simpleform.html | 21 - .../kontor/templates/tysc/manufacturers.html | 58 --- flask/kontor/templates/tysc/players.html | 60 --- flask/kontor/templates/tysc/positions.html | 62 --- flask/kontor/templates/tysc/sports.html | 58 --- flask/kontor/templates/tysc/teams.html | 60 --- flask/kontor/tysc/__init__.py | 377 ---------------- flask/kontor/tysc/forms.py | 76 ---- flask/kontor/tysc/models.py | 155 ------- flask/kontor/tysc/views.py | 405 ------------------ flask/kontor/version.py | 2 - flask/pylint.cfg | 382 ----------------- flask/requirements.txt | 11 - flask/tests/__init__.py | 38 -- flask/tests/test_comics_model.py | 51 --- flask/tests/test_comics_view.py | 44 -- flask/tests/test_config.txt | 4 - flask/tests/test_kontor_model.py | 14 - flask/tests/test_kontor_view.py | 58 --- flask/tests/test_library_model.py | 49 --- flask/tests/test_library_view.py | 44 -- flask/tests/test_tysc_model.py | 53 --- flask/tests/test_tysc_view.py | 110 ----- 82 files changed, 562 insertions(+), 4270 deletions(-) create mode 100644 flask/.gitignore create mode 100644 flask/README.md create mode 100644 flask/app.py delete mode 100644 flask/config.py delete mode 100644 flask/docs/Makefile delete mode 100644 flask/docs/conf.py delete mode 100644 flask/docs/index.rst delete mode 100644 flask/kontor/admin/__init__.py delete mode 100644 flask/kontor/admin/forms.py delete mode 100644 flask/kontor/admin/views.py create mode 100644 flask/kontor/api/__init__.py create mode 100644 flask/kontor/api/routes.py delete mode 100644 flask/kontor/auth/forms.py create mode 100644 flask/kontor/auth/models.py delete mode 100644 flask/kontor/auth/views.py create mode 100644 flask/kontor/comics/api.py delete mode 100644 flask/kontor/comics/forms.py delete mode 100644 flask/kontor/comics/models.py create mode 100644 flask/kontor/comics/routes.py create mode 100644 flask/kontor/comics/templates/comics/list.html create mode 100644 flask/kontor/comics/templates/comics/view.html delete mode 100644 flask/kontor/comics/views.py create mode 100644 flask/kontor/config.py create mode 100644 flask/kontor/extensions.py delete mode 100644 flask/kontor/home/__init__.py delete mode 100644 flask/kontor/home/views.py delete mode 100644 flask/kontor/library/__init__.py delete mode 100644 flask/kontor/library/forms.py delete mode 100644 flask/kontor/library/models.py delete mode 100644 flask/kontor/library/views.py create mode 100644 flask/kontor/main/__init__.py create mode 100644 flask/kontor/main/routes.py create mode 100644 flask/kontor/media/__init__.py create mode 100644 flask/kontor/media/api.py create mode 100644 flask/kontor/media/models.py create mode 100644 flask/kontor/media/routes.py create mode 100644 flask/kontor/media/templates/media/mediafile_detail.html create mode 100644 flask/kontor/media/templates/media/mediafile_list.html delete mode 100644 flask/kontor/office/__init__.py delete mode 100644 flask/kontor/static/css/style.css delete mode 100644 flask/kontor/templates/admin/users/user.html delete mode 100644 flask/kontor/templates/admin/users/users.html delete mode 100644 flask/kontor/templates/auth/login.html delete mode 100644 flask/kontor/templates/auth/register.html delete mode 100644 flask/kontor/templates/comics/artists.html delete mode 100644 flask/kontor/templates/comics/comics.html delete mode 100644 flask/kontor/templates/comics/publishers.html delete mode 100644 flask/kontor/templates/home/admin_dashboard.html delete mode 100644 flask/kontor/templates/home/dashboard.html delete mode 100644 flask/kontor/templates/home/index.html create mode 100644 flask/kontor/templates/index.html delete mode 100644 flask/kontor/templates/library/authors.html delete mode 100644 flask/kontor/templates/library/books.html delete mode 100644 flask/kontor/templates/library/publishers.html delete mode 100644 flask/kontor/templates/simpleform.html delete mode 100644 flask/kontor/templates/tysc/manufacturers.html delete mode 100644 flask/kontor/templates/tysc/players.html delete mode 100644 flask/kontor/templates/tysc/positions.html delete mode 100644 flask/kontor/templates/tysc/sports.html delete mode 100644 flask/kontor/templates/tysc/teams.html delete mode 100644 flask/kontor/tysc/__init__.py delete mode 100644 flask/kontor/tysc/forms.py delete mode 100644 flask/kontor/tysc/models.py delete mode 100644 flask/kontor/tysc/views.py delete mode 100644 flask/kontor/version.py delete mode 100644 flask/pylint.cfg delete mode 100644 flask/requirements.txt delete mode 100644 flask/tests/__init__.py delete mode 100644 flask/tests/test_comics_model.py delete mode 100644 flask/tests/test_comics_view.py delete mode 100644 flask/tests/test_config.txt delete mode 100644 flask/tests/test_kontor_model.py delete mode 100644 flask/tests/test_kontor_view.py delete mode 100644 flask/tests/test_library_model.py delete mode 100644 flask/tests/test_library_view.py delete mode 100644 flask/tests/test_tysc_model.py delete mode 100644 flask/tests/test_tysc_view.py diff --git a/flask/.gitignore b/flask/.gitignore new file mode 100644 index 0000000..259ed2a --- /dev/null +++ b/flask/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +.idea diff --git a/flask/README.md b/flask/README.md new file mode 100644 index 0000000..6aad8a5 --- /dev/null +++ b/flask/README.md @@ -0,0 +1,2 @@ +# Kontor Flask + diff --git a/flask/app.py b/flask/app.py new file mode 100644 index 0000000..dc827b3 --- /dev/null +++ b/flask/app.py @@ -0,0 +1,5 @@ +from kontor import create_app + +if __name__ == '__main__': + app = create_app() + app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/flask/config.py b/flask/config.py deleted file mode 100644 index 4ea5e78..0000000 --- a/flask/config.py +++ /dev/null @@ -1,47 +0,0 @@ -# config.py - - -class Config(object): - """ - Common configurations - """ - - # Put any configurations here that are common across all environments - host = '127.0.0.1' - port = 8500 - database = 'kontor' - - -class DevelopmentConfig(Config): - """ - Development configurations - """ - - DEBUG = True - host = '0.0.0.0' - database = 'kontor_dev' - - -class ProductionConfig(Config): - """ - Production configurations - """ - - DEBUG = False - -class TestingConfig(Config): - """ - Testing configurations - """ - - TESTING = True - DEBUG = True - port = 8600 - database = 'kontor_test' - -app_config = { - 'development': DevelopmentConfig, - 'production': ProductionConfig, - 'testing': TestingConfig -} - diff --git a/flask/docs/Makefile b/flask/docs/Makefile deleted file mode 100644 index d7218b4..0000000 --- a/flask/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXPROJ = KontorFlask -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/flask/docs/conf.py b/flask/docs/conf.py deleted file mode 100644 index 7278f2d..0000000 --- a/flask/docs/conf.py +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Kontor Flask documentation build configuration file, created by -# sphinx-quickstart on Mon Nov 6 08:53:04 2017. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.mathjax', - 'sphinx.ext.ifconfig', - 'sphinx.ext.viewcode'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = 'Kontor Flask' -copyright = '2017, Thomas Peetz' -author = 'Thomas Peetz' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '0.1' -# The full version, including alpha/beta/rc tags. -release = '0.0.7' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = 'de' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'alabaster' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# This is required for the alabaster theme -# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars -html_sidebars = { - '**': [ - 'relations.html', # needs 'show_related': True theme option to display - 'searchbox.html', - ] -} - - -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = 'KontorFlaskdoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'KontorFlask.tex', 'Kontor Flask Documentation', - 'Thomas Peetz', 'manual'), -] - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'kontorflask', 'Kontor Flask Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'KontorFlask', 'Kontor Flask Documentation', - author, 'KontorFlask', 'One line description of project.', - 'Miscellaneous'), -] - - - -# -- Options for Epub output ---------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = project -epub_author = author -epub_publisher = author -epub_copyright = copyright - -# The unique identifier of the text. This can be a ISBN number -# or the project homepage. -# -# epub_identifier = '' - -# A unique identification for the text. -# -# epub_uid = '' - -# A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] - - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} diff --git a/flask/docs/index.rst b/flask/docs/index.rst deleted file mode 100644 index ca4df98..0000000 --- a/flask/docs/index.rst +++ /dev/null @@ -1,20 +0,0 @@ -.. Kontor Flask documentation master file, created by - sphinx-quickstart on Mon Nov 6 08:53:04 2017. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to Kontor Flask's documentation! -======================================== - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/flask/kontor/__init__.py b/flask/kontor/__init__.py index 961e132..f8cb55c 100644 --- a/flask/kontor/__init__.py +++ b/flask/kontor/__init__.py @@ -1,74 +1,61 @@ -""" -Module kontor implements Kontor application. -""" +from flask import Flask, render_template +from flask_jwt_extended import JWTManager -from flask import Flask -from flask_bootstrap import Bootstrap -from flask_login import LoginManager -from pymodm import connect +from kontor import config +from kontor.extensions import db, ma +from logging.config import dictConfig -# local imports -from config import app_config -from .version import __version__ +dictConfig({ + 'version': 1, + 'formatters': {'default': { + 'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', + }}, + 'handlers': {'wsgi': { + 'class': 'logging.StreamHandler', + 'stream': 'ext://flask.logging.wsgi_errors_stream', + 'formatter': 'default' + }}, + 'root': { + 'level': 'INFO', + 'handlers': ['wsgi'] + } +}) + +app = Flask(__name__) -_LOGIN_MANAGER_ = LoginManager() +def create_app(config_class=config.Config): + app.config.from_object(config_class) + db.init_app(app) + ma.init_app(app) + # Initialize Flask extensions here + app.config["JWT_SECRET_KEY"] = "super-secret" # Change this! + jwt = JWTManager(app) -def get_host(config_name): - """ - Returns host address from configuration. - :param config_name: - :return: host address - """ - host = app_config[config_name].host - return host + with app.app_context(): + # db.create_all() + db.reflect() + # Register blueprints here + from kontor.main import bp as main_bp + app.register_blueprint(main_bp) + from kontor.comics import comics_bp + app.register_blueprint(comics_bp, url_prefix='/comics') + from kontor.api import api_bp + app.register_blueprint(api_bp, url_prefix='/api/v1') + from kontor.comics import comics_api + app.register_blueprint(comics_api, url_prefix='/api/v1/comics') -def get_port(config_name): - """ - Returns port number from configuration. - :param config_name: - :return: port number - """ - port = app_config[config_name].port - return port - - -def create_app(config_name): - """ - Returns Flask application. - :param config_name: - :return: Flask application - """ - app = Flask(__name__, instance_relative_config=True) - app.config.from_object(app_config[config_name]) - app.config.from_pyfile('config.py') - - database = "mongodb://localhost:27017/{}".format(app_config[config_name].database) - connect(database, alias="kontor") - - Bootstrap(app) - _LOGIN_MANAGER_.init_app(app) - _LOGIN_MANAGER_.login_message = "You must be logged in to access this page." - _LOGIN_MANAGER_.login_view = "auth.login" - - from .admin.views import ADMIN as ADMIN_BLUEPRINT - app.register_blueprint(ADMIN_BLUEPRINT, url_prefix='/admin') - - from .auth.views import AUTH as AUTH_BLUEPRINT - app.register_blueprint(AUTH_BLUEPRINT) - - from .home.views import HOME as HOME_BLUEPRINT - app.register_blueprint(HOME_BLUEPRINT) - - from .comics.views import COMIC as COMIC_BLUEPRINT - app.register_blueprint(COMIC_BLUEPRINT, url_prefix='/comics') - - from .library.views import LIBRARY as LIBRARY_BLUEPRINT - app.register_blueprint(LIBRARY_BLUEPRINT, url_prefix='/library') - - from .tysc.views import TYSC as TYSC_BLUEPRINT - app.register_blueprint(TYSC_BLUEPRINT, url_prefix='/tysc') + from kontor.media import media_bp + app.register_blueprint(media_bp, url_prefix='/media') + from kontor.media import media_api + app.register_blueprint(media_api, url_prefix='/api/v1/media') + # from kontor.auth.auth import auth_bp + # from kontor.cart.cart import cart_bp + # from kontor.general.general import general_bp + # app.register_blueprint(auth_bp) + # app.register_blueprint(cart_bp, url_prefix='/cart') + # app.register_blueprint(general_bp) return app diff --git a/flask/kontor/admin/__init__.py b/flask/kontor/admin/__init__.py deleted file mode 100644 index ca00a4b..0000000 --- a/flask/kontor/admin/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -""" -Module admin implements administration functions. -""" -from . import views diff --git a/flask/kontor/admin/forms.py b/flask/kontor/admin/forms.py deleted file mode 100644 index d05cfaf..0000000 --- a/flask/kontor/admin/forms.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Define form to edit users -""" -from flask_wtf import FlaskForm -from wtforms import StringField, SubmitField -from wtforms.validators import DataRequired - - -class UserEditForm(FlaskForm): - """ - Form for admin to edit users - """ - email = StringField('Email', validators=[DataRequired()]) - username = StringField('Username', validators=[DataRequired()]) - first_name = StringField('First Name', validators=[DataRequired()]) - last_name = StringField('Last Name', validators=[DataRequired()]) - submit = SubmitField('Submit') diff --git a/flask/kontor/admin/views.py b/flask/kontor/admin/views.py deleted file mode 100644 index e96d8f0..0000000 --- a/flask/kontor/admin/views.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -Define routing rules for managing users -""" -from flask import abort, Blueprint, flash, redirect, render_template, url_for -from flask_login import current_user, login_required - -from .forms import UserEditForm -from ..models import User - -ADMIN = Blueprint('admin', __name__) - - -def check_admin(): - """ - Prevent non-admins from accessing the page - """ - if not current_user.is_admin: - abort(403) - - -# User Views -@ADMIN.route('/users') -@login_required -def list_users(): - """ - List all users - """ - check_admin() - - users = User.objects.all() - return render_template('admin/users/users.html', - users=users, title='Users') - - -@ADMIN.route('/users/edit/', methods=['GET', 'POST']) -@login_required -def edit_user(user_id): - """ - Edit an user. - """ - check_admin() - - user = User.objects.get({'username': user_id}) - - # prevent admin from being assigned a department or role - if user.is_admin: - abort(403) - - form = UserEditForm(obj=user) - if form.validate_on_submit(): - user.email = form.email.data - user.username = form.username.data - user.first_name = form.first_name.data - user.last_name = form.last_name.data - user.save() - flash('You have successfully edited an user.') - - # redirect to the roles page - return redirect(url_for('admin.list_users')) - - return render_template('admin/users/user.html', - user=user, form=form, - title='Edit User') diff --git a/flask/kontor/api/__init__.py b/flask/kontor/api/__init__.py new file mode 100644 index 0000000..b26fcb6 --- /dev/null +++ b/flask/kontor/api/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +api_bp = Blueprint('api_bp', __name__) + +from kontor.api import routes diff --git a/flask/kontor/api/routes.py b/flask/kontor/api/routes.py new file mode 100644 index 0000000..4e9b489 --- /dev/null +++ b/flask/kontor/api/routes.py @@ -0,0 +1,30 @@ +from flask import jsonify, request +from flask_jwt_extended import jwt_required, get_jwt_identity, create_access_token + +from kontor.api import api_bp + + +@api_bp.route('/') +def index(): + modules = ['comics'] + return jsonify(modules) + + +# Create a route to authenticate your users and return JWTs. The +# create_access_token() function is used to actually generate the JWT. +@api_bp.route("/login", methods=["POST"]) +def login(): + username = request.json.get("username", None) + password = request.json.get("password", None) + if username != "test" or password != "test": + return jsonify({"msg": "Bad username or password"}), 401 + + access_token = create_access_token(identity=username) + return jsonify(access_token=access_token) + + +@api_bp.route('/protected', methods=['GET']) +@jwt_required() +def protected(): + current_user = get_jwt_identity() + return {'message': f'Hello, {current_user}!'} diff --git a/flask/kontor/auth/__init__.py b/flask/kontor/auth/__init__.py index 2d9b1ca..9da7988 100644 --- a/flask/kontor/auth/__init__.py +++ b/flask/kontor/auth/__init__.py @@ -1,4 +1,22 @@ -""" -Module auth implements authentication functions. -""" -from . import views +import bcrypt +from flask import session +from flask_httpauth import HTTPBasicAuth + +from kontor import app +from kontor.auth.models import User + +auth = HTTPBasicAuth() + + +@auth.verify_password +def verify_password(username, password): + if username is None: + return False + # Add your authentication logic here + app.logger.info("login user %s", username) + user = User.query.filter_by(user_name=username).first() + app.logger.info("User: %s", user) + app.logger.info("Stored Password: '%s' Hashed Password: '%s'", user.password, bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())) + if 'user_name' in session and session['user_name'] == username: + return True + return False \ No newline at end of file diff --git a/flask/kontor/auth/forms.py b/flask/kontor/auth/forms.py deleted file mode 100644 index 6e4d6dd..0000000 --- a/flask/kontor/auth/forms.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Contains forms for authentication. -""" -from flask_wtf import FlaskForm -from wtforms import PasswordField, StringField, SubmitField, ValidationError -from wtforms.validators import DataRequired, Email, EqualTo - -from ..models import User - - -class RegistrationForm(FlaskForm): - """ - Form for users to create new account - """ - email = StringField('Email', validators=[DataRequired(), Email()]) - username = StringField('Username', validators=[DataRequired()]) - first_name = StringField('First Name', validators=[DataRequired()]) - last_name = StringField('Last Name', validators=[DataRequired()]) - password = PasswordField('Password', validators=[DataRequired(), - EqualTo('confirm_password') - ]) - confirm_password = PasswordField('Confirm Password') - submit = SubmitField('Register') - - def validate_email(self, field): - """ - Check if email is already in use. - :param field: - :return: - """ - if User.objects.raw({'email': field.data}).count() > 0: - raise ValidationError('Email is already in use.') - - def validate_username(self, field): - """ - check if username is already in use. - :param field: - :return: - """ - if User.objects.raw({'username': field.data}).count() > 0: - raise ValidationError('Username is already in use.') - - -class LoginForm(FlaskForm): - """ - Form for users to login - """ - email = StringField('Email', validators=[DataRequired(), Email()]) - password = PasswordField('Password', validators=[DataRequired()]) - submit = SubmitField('Login') diff --git a/flask/kontor/auth/models.py b/flask/kontor/auth/models.py new file mode 100644 index 0000000..fc3b8d4 --- /dev/null +++ b/flask/kontor/auth/models.py @@ -0,0 +1,45 @@ +from kontor.extensions import db, ma +from sqlalchemy.sql import func + + +class User(db.Model): + # __table__ = db.metadata.tables["publisher"] + __tablename__ = "user" + __table_args__ = {'extend_existing': True} + id = db.Column(db.String, primary_key=True) + created_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + last_modified_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + version = db.Column(db.Integer) + enabled = db.Column(db.SmallInteger) + email = db.Column(db.String) + first_name = db.Column(db.String) + last_name = db.Column(db.String) + user_name = db.Column(db.String) + password = db.Column(db.String) + token = db.Column(db.String) + token_expired = db.Column(db.SmallInteger) + + def is_token_valid(self): + return self.review == 'b\x01' + + def is_user_enabled(self): + return self.should_download == 'b\x01' + +class Role(db.Model): + __tablename__ = "role" + __table_args__ = {'extend_existing': True} + id = db.Column(db.String, primary_key=True) + created_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + last_modified_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + version = db.Column(db.Integer) + name = db.Column(db.String) + +class AuthorizationMatrix(db.Model): + __tablename__ = "authorization_matrix" + __table_args__ = {'extend_existing': True} + id = db.Column(db.String, primary_key=True) + created_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + last_modified_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + version = db.Column(db.Integer) + role_id = db.Column(db.String, db.ForeignKey("role.id")) + user_id = db.Column(db.String, db.ForeignKey("user.id")) diff --git a/flask/kontor/auth/views.py b/flask/kontor/auth/views.py deleted file mode 100644 index 217ae18..0000000 --- a/flask/kontor/auth/views.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -Define routing rules for registering users and login and logout. -""" -from flask import Blueprint, flash, redirect, render_template, url_for -from flask_login import login_required, login_user, logout_user -from .forms import LoginForm, RegistrationForm -from ..models import User - -AUTH = Blueprint('auth', __name__) - - -@AUTH.route('/register', methods=['GET', 'POST']) -def register(): - """ - Handle requests to the /register route - Add an employee to the database through the registration form - """ - form = RegistrationForm() - if form.validate_on_submit(): - user = User() - user.email = form.email.data - user.username = form.username.data - user.first_name = form.first_name.data - user.last_name = form.last_name.data - user.password = form.password.data - # add employee to the database - user.save() - flash('You have successfully registered! You may now login.') - - # redirect to the login page - return redirect(url_for('auth.login')) - - # load registration template - return render_template('auth/register.html', form=form, title='Register') - - -@AUTH.route('/login', methods=['GET', 'POST']) -def login(): - """ - Handle requests to the /login route - Validate given email with matching password - :return: - """ - form = LoginForm() - if form.validate_on_submit(): - - # check whether employee exists in the database and whether - # the password entered matches the password in the database - user = User.objects.get({'email': form.email.data}) - if user is not None and user.verify_password( - form.password.data): - # log employee in - login_user(user) - - # redirect to the appropriate dashboard page - if user.is_admin: - return redirect(url_for('home.admin_dashboard')) - else: - return redirect(url_for('home.dashboard')) - - # when login details are incorrect - else: - flash('Invalid email or password.') - - # load login template - return render_template('auth/login.html', form=form, title='Login') - -@AUTH.route('/logout') -@login_required -def logout(): - """ - Handle requests to the /logout route - Log an employee out through the logout link - """ - logout_user() - flash('You have successfully been logged out.') - - # redirect to the login page - return redirect(url_for('auth.login')) diff --git a/flask/kontor/comics/__init__.py b/flask/kontor/comics/__init__.py index e947d82..12bb371 100644 --- a/flask/kontor/comics/__init__.py +++ b/flask/kontor/comics/__init__.py @@ -1,3 +1,9 @@ -""" -Define routing rules for comic related information -""" +from flask import Blueprint + +comics_bp = Blueprint('comics_bp', __name__, + template_folder='templates', + static_folder='static', static_url_path='assets') +comics_api = Blueprint('comics_api', __name__) + +from kontor.comics import routes +from kontor.comics import api diff --git a/flask/kontor/comics/api.py b/flask/kontor/comics/api.py new file mode 100644 index 0000000..22bca28 --- /dev/null +++ b/flask/kontor/comics/api.py @@ -0,0 +1,28 @@ +from flask import Blueprint, render_template, jsonify + +from kontor.comics import comics_api +from kontor.models import Comic, comics_schema, Publisher, comic_schema, publisher_schema, publishers_schema + + +@comics_api.route('/') +def index(): + comics = Comic.query.all() + return comics_schema.dump(comics) + + +@comics_api.route('/comic/') +def view(comic_id): + comic = Comic.query.get(comic_id) + return comic_schema.dump(comic) + + +@comics_api.route('/publisher/') +def publisher_all(): + publishers = Publisher.query.all() + return publishers_schema.dump(publishers) + + +@comics_api.route('/publisher/') +def publisher_detail(publisher_id): + publisher = Publisher.query.get(publisher_id) + return publisher_schema.dump(publisher) \ No newline at end of file diff --git a/flask/kontor/comics/forms.py b/flask/kontor/comics/forms.py deleted file mode 100644 index 0bae45f..0000000 --- a/flask/kontor/comics/forms.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Define form to edit publisher, artists and comics""" -from flask_wtf import FlaskForm -from wtforms import StringField, SubmitField, BooleanField, SelectField -from wtforms.validators import DataRequired -from bson import ObjectId - - -class PublisherForm(FlaskForm): - """ - Form to add and edit a Comic publisher - """ - name = StringField('Name', validators=[DataRequired()]) - submit = SubmitField('Submit') - - -class ArtistForm(FlaskForm): - """ - Form to add and edit a Comic publisher - """ - name = StringField('Name', validators=[DataRequired()]) - submit = SubmitField('Submit') - - -class ComicForm(FlaskForm): - """ - Form to add and edit Comics - """ - title = StringField('Title', validators=[DataRequired()]) - publisher = SelectField('Publisher', coerce=ObjectId) - current_order = BooleanField('Current Order') - submit = SubmitField('Submit') diff --git a/flask/kontor/comics/models.py b/flask/kontor/comics/models.py deleted file mode 100644 index e39e744..0000000 --- a/flask/kontor/comics/models.py +++ /dev/null @@ -1,51 +0,0 @@ -"""This modules declares the model for Comic related information.""" - -from flask import current_app -from pymongo.write_concern import WriteConcern -from pymodm import MongoModel, fields - - -class Publisher(MongoModel): - """Class Publisher represents a publisher of a comic.""" - name = fields.CharField() - - def __str__(self): - return "Publisher({})".format(self.name) - - @property - def comics(self): - """ - Return list of comics which has reference to this publisher - :return: - """ - comics = Comic.objects.raw({'publisher': self.pk}) - current_app.logger.debug(comics) - return comics - - class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) - - -class Artist(MongoModel): - """Class Artist represents a comic artist.""" - name = fields.CharField() - - class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) - - -class Comic(MongoModel): - """Class Comic represents a comic.""" - title = fields.CharField() - publisher = fields.ReferenceField(Publisher) - current_order = fields.BooleanField(default=False) - completed = fields.BooleanField(default=False) - - class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) diff --git a/flask/kontor/comics/routes.py b/flask/kontor/comics/routes.py new file mode 100644 index 0000000..0b0f53e --- /dev/null +++ b/flask/kontor/comics/routes.py @@ -0,0 +1,16 @@ +from flask import Blueprint, render_template + +from kontor.comics import comics_bp +from kontor.models import Comic + + +@comics_bp.route('/') +def index(): + comics = Comic.query.all() + return render_template('comics/list.html', comics=comics) + + +@comics_bp.route('/comic/') +def view(comic_id): + comic = Comic.query.get(comic_id) + return render_template('comics/view.html', comic=comic) diff --git a/flask/kontor/comics/templates/comics/list.html b/flask/kontor/comics/templates/comics/list.html new file mode 100644 index 0000000..c0f4922 --- /dev/null +++ b/flask/kontor/comics/templates/comics/list.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% block content %} + +

{% block title %} Comics {% endblock %}

+
+
+

Comics Blueprint

+ {% for comic in comics %} +
+ +

+ {{ comic.title }} +

+
+

{{ comic.publisher.name }}

+
+

Created

+

{{ comic.created_date }}

+

Modified

+

{{ comic.last_modified_date }}

+
+

Version: {{ comic.version }}

+

Review: {{ comic.review }}

+
+ {% endfor %} +
+{% endblock %} diff --git a/flask/kontor/comics/templates/comics/view.html b/flask/kontor/comics/templates/comics/view.html new file mode 100644 index 0000000..b70da14 --- /dev/null +++ b/flask/kontor/comics/templates/comics/view.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} + +{% block content %} + +

{% block title %} Comics {% endblock %}

+
+
+

Comics Blueprint

+
+ +

+ {{ comic.title }} +

+
+ +

{{ comic.id }}

+
+
+

Created

+

{{ comic.created_date }}

+

Modified

+

{{ comic.last_modified_date }}

+
+

Version: {{ comic.version }}

+
+
+{% endblock %} diff --git a/flask/kontor/comics/views.py b/flask/kontor/comics/views.py deleted file mode 100644 index 090b032..0000000 --- a/flask/kontor/comics/views.py +++ /dev/null @@ -1,222 +0,0 @@ -""" -Define routing rules for comics, publisher and artists -""" -from flask import Blueprint, flash, redirect, render_template, url_for -from flask_login import login_required -from bson import ObjectId -from pymongo.errors import PyMongoError -from .forms import ComicForm, PublisherForm, ArtistForm -from .models import Comic, Publisher, Artist - - -COMIC = Blueprint('comic', __name__) - - -@COMIC.route('/artists') -@login_required -def list_artists(): - """ - List all artists - :return: - """ - artists = Artist.objects.all() - return render_template('comics/artists.html', - artists=artists, title="Artists") - - -@COMIC.route('/artists/edit/', methods=['GET', 'POST']) -@login_required -def edit_artist(artist_id): - """ - Edit a comic artist - """ - artist = Artist.objects.get({'_id': ObjectId(artist_id)}) - form = ArtistForm(obj=artist) - if form.validate_on_submit(): - artist.name = form.name.data - artist.save() - flash('You have successfully edited the artist.') - return redirect(url_for('comic.list_artists')) - form.name.data = artist.name - return render_template('simpleform.html', action="Edit", - form=form, title="Edit Artist") - - -@COMIC.route('/artists/add', methods=['GET', 'POST']) -@login_required -def add_artist(): - """ - Add a artist - :return: - """ - form = ArtistForm() - if form.validate_on_submit(): - artist = Artist() - artist.name = form.name.data - try: - # add publisher to the database - artist.save() - flash('You have successfully added a new artist.') - except PyMongoError: - # in case publisher name already exists - flash('Error: artist name already exists.') - return redirect(url_for('comic.list_artists')) - return render_template('simpleform.html', action="Add", - form=form, title="Add Artist") - - -@COMIC.route('/artists/delete/', methods=['GET', 'POST']) -@login_required -def delete_artist(artist_id): - """ - Delete a comic artist - :param artist_id: - :return: - """ - artist = Artist.objects.raw({'_id': ObjectId(artist_id)}) - if artist: - artist.delete() - flash('You have successfully deleted the comic artist.') - return redirect(url_for('comic.list_artists')) - - -@COMIC.route('/publishers') -@login_required -def list_publishers(): - """ - List all publishers - :return: - """ - publishers = Publisher.objects.all() - return render_template('comics/publishers.html', - publishers=publishers, title="Publishers") - - -@COMIC.route('/publishers/add', methods=['GET', 'POST']) -@login_required -def add_publisher(): - """ - Add a publisher to the database - :return: - """ - form = PublisherForm() - if form.validate_on_submit(): - publisher = Publisher() - publisher.name = form.name.data - try: - # add publisher to the database - publisher.save() - flash('You have successfully added a new publisher.') - except PyMongoError: - # in case publisher name already exists - flash('Error: publisher name already exists.') - return redirect(url_for('comic.list_publishers')) - return render_template('simpleform.html', action="Add", - form=form, title="Add Publisher") - - -@COMIC.route('/publishers/edit/', methods=['GET', 'POST']) -@login_required -def edit_publisher(publisher_id): - """ - Edit a publisher - """ - publisher = Publisher.objects.get({'_id': ObjectId(publisher_id)}) - form = PublisherForm(obj=publisher) - if form.validate_on_submit(): - publisher.name = form.name.data - publisher.save() - flash('You have successfully edited the publisher.') - return redirect(url_for('comic.list_publishers')) - form.name.data = publisher.name - return render_template('simpleform.html', action="Edit", - form=form, title="Edit Publisher") - - -@COMIC.route('/publishers/delete/', methods=['GET', 'POST']) -@login_required -def delete_publisher(publisher_id): - """ - Delete a publisher - :param publisher_id: ObjectId of publisher - :return: - """ - publisher = Publisher.objects.raw({'_id': ObjectId(publisher_id)}) - if publisher: - publisher.delete() - flash('You have successfully deleted the publisher.') - return redirect(url_for('comic.list_publishers')) - - -@COMIC.route('/comics') -@login_required -def list_comics(): - """ - List all comics - :return: - """ - comics = Comic.objects.all() - return render_template('comics/comics.html', - comics=comics, title="Comics") - - -@COMIC.route('/comics/edit/', methods=['GET', 'POST']) -@login_required -def edit_comic(comic_id): - """ - Edit a comic - """ - comic = Comic.objects.get({'_id': ObjectId(comic_id)}) - form = ComicForm(obj=comic) - form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] - form.publisher.default = comic.publisher.pk - form.publisher.process_data(comic.publisher.pk) - if form.validate_on_submit(): - comic.title = form.title.data - comic.current_order = form.current_order.data - comic.publisher = form.publisher.data - comic.save() - flash('You have successfully edited the comic.') - return redirect(url_for('comic.list_comics')) - form.title.data = comic.title - form.current_order.data = comic.current_order - return render_template('simpleform.html', action="Edit", - form=form, title="Edit Comic") - - -@COMIC.route('/comics/add', methods=['GET', 'POST']) -@login_required -def add_comic(): - """ - Add a comic - :return: - """ - form = ComicForm() - form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] - if form.validate_on_submit(): - comic = Comic() - comic.title = form.title.data - comic.publisher = form.publisher.data - try: - comic.save() - flash('You have successfully added a new comic.') - except PyMongoError: - flash('Error: comic title already exists.') - return redirect(url_for('comic.list_comics')) - return render_template('simpleform.html', action="Add", - form=form, title="Add Comic") - - -@COMIC.route('/comics/delete/', methods=['GET', 'POST']) -@login_required -def delete_comic(comic_id): - """ - Delete a comic - :param comic_id: - :return: - """ - comic = Comic.objects.raw({'_id': ObjectId(comic_id)}) - if comic: - comic.delete() - flash('You have successfully deleted the comic.') - return redirect(url_for('comic.list_comics')) diff --git a/flask/kontor/config.py b/flask/kontor/config.py new file mode 100644 index 0000000..8382f66 --- /dev/null +++ b/flask/kontor/config.py @@ -0,0 +1,10 @@ +import os + +basedir = os.path.abspath(os.path.dirname(__file__)) + + +class Config: + SECRET_KEY = os.environ.get('SECRET_KEY') + # SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI') or 'sqlite:///' + os.path.join(basedir, 'kontor.db') + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI') or 'mariadb+mariadbconnector://kontor:kontor@localhost/kontor' + SQLALCHEMY_TRACK_MODIFICATIONS = False diff --git a/flask/kontor/extensions.py b/flask/kontor/extensions.py new file mode 100644 index 0000000..461bd5d --- /dev/null +++ b/flask/kontor/extensions.py @@ -0,0 +1,5 @@ +import flask_sqlalchemy +from flask_marshmallow import Marshmallow + +db = flask_sqlalchemy.SQLAlchemy() +ma = Marshmallow() diff --git a/flask/kontor/home/__init__.py b/flask/kontor/home/__init__.py deleted file mode 100644 index d4468a9..0000000 --- a/flask/kontor/home/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Module for the Kontor homepage. -""" diff --git a/flask/kontor/home/views.py b/flask/kontor/home/views.py deleted file mode 100644 index 4e4d539..0000000 --- a/flask/kontor/home/views.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -Define routing rules for homepage and dashboards -""" - -from flask import Blueprint, render_template, abort -from flask_login import login_required, current_user - -HOME = Blueprint('home', __name__) - - -@HOME.route('/') -def homepage(): - """ - Render the homepage template on the / route - """ - return render_template('home/index.html', title="Welcome") - - -@HOME.route('/dashboard') -@login_required -def dashboard(): - """ - Render the dashboard template on the /dashboard route - """ - return render_template('home/dashboard.html', title="Dashboard") - - -@HOME.route('/admin/dashboard') -@login_required -def admin_dashboard(): - """ - Render the admin_dashboard template on the /admin/dashboard route - :return: - """ - # prevent non-admins from accessing the page - if not current_user.is_admin: - abort(403) - - return render_template('home/admin_dashboard.html', title="Dashboard") diff --git a/flask/kontor/library/__init__.py b/flask/kontor/library/__init__.py deleted file mode 100644 index 31eb0bc..0000000 --- a/flask/kontor/library/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Define routing rules for library related information -""" diff --git a/flask/kontor/library/forms.py b/flask/kontor/library/forms.py deleted file mode 100644 index a080781..0000000 --- a/flask/kontor/library/forms.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Define form to edit publisher, artists and books -""" -from flask_wtf import FlaskForm -from wtforms import StringField, SubmitField, SelectField, IntegerField -from wtforms.validators import DataRequired, Optional, NumberRange -from bson import ObjectId - - -class PublisherForm(FlaskForm): - """ - Form to add and edit a Comic publisher - """ - name = StringField('Name', validators=[DataRequired()]) - submit = SubmitField('Submit') - - -class AuthorForm(FlaskForm): - """ - Form to add and edit a Comic publisher - """ - name = StringField('Name', validators=[DataRequired()]) - submit = SubmitField('Submit') - - -class BookForm(FlaskForm): - """ - Form to add and edit Comics - """ - title = StringField('Title', validators=[DataRequired()]) - author = SelectField('Author', coerce=ObjectId) - publisher = SelectField('Publisher', coerce=ObjectId) - isbn = StringField('ISBN', validators=[Optional()]) - year = IntegerField('Year', validators=[NumberRange(min=1970, max=2050)]) - submit = SubmitField('Submit') diff --git a/flask/kontor/library/models.py b/flask/kontor/library/models.py deleted file mode 100644 index 7ca5c58..0000000 --- a/flask/kontor/library/models.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -This module declares the model for the library related information. -""" -from pymongo.write_concern import WriteConcern -from pymodm import MongoModel, fields - - -class Publisher(MongoModel): - """ - Class Publisher represents a publisher of a book. - """ - name = fields.CharField() - - def __str__(self): - return "Publisher({})".format(self.name) - - class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) - - -class Author(MongoModel): - """ - Class Author represents an author of a book. - """ - name = fields.CharField() - - def __str__(self): - return "Author({})".format(self.name) - - class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) - - -class Book(MongoModel): - """ - Class book represents a book. - """ - title = fields.CharField() - author = fields.ReferenceField(Author) - publisher = fields.ReferenceField(Publisher) - isbn = fields.CharField(blank=True) - year = fields.IntegerField(blank=True) - edition = fields.CharField() - - def __str__(self): - return "Book({})".format(self.title) - - class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) diff --git a/flask/kontor/library/views.py b/flask/kontor/library/views.py deleted file mode 100644 index 1b53eee..0000000 --- a/flask/kontor/library/views.py +++ /dev/null @@ -1,228 +0,0 @@ -"""Define routing rules for book publishers""" -from flask import Blueprint, flash, redirect, render_template, url_for -from flask_login import login_required -from bson import ObjectId -from pymongo.errors import PyMongoError -from .forms import PublisherForm, AuthorForm, BookForm -from .models import Publisher, Author, Book - - -LIBRARY = Blueprint('library', __name__) - - -@LIBRARY.route('/publishers') -@login_required -def list_publishers(): - """ - List all publishers - :return: - """ - publishers = Publisher.objects.all() - return render_template('library/publishers.html', - publishers=publishers, title="Publishers") - - -@LIBRARY.route('/publishers/add', methods=['GET', 'POST']) -@login_required -def add_publisher(): - """ - Add a publisher to the database - :return: - """ - form = PublisherForm() - if form.validate_on_submit(): - publisher = Publisher() - publisher.name = form.name.data - try: - publisher.save() - flash('You have successfully added a new publisher.') - except PyMongoError: - flash('Error: publisher name already exists.') - return redirect(url_for('library.list_publishers')) - return render_template('simpleform.html', action="Add", - form=form, title="Add Publisher") - - -@LIBRARY.route('/publishers/edit/', methods=['GET', 'POST']) -@login_required -def edit_publisher(publisher_id): - """ - Edit a publisher - """ - publisher = Publisher.objects.get({'_id': ObjectId(publisher_id)}) - form = PublisherForm(obj=publisher) - if form.validate_on_submit(): - publisher.name = form.name.data - publisher.save() - flash('You have successfully edited the publisher.') - return redirect(url_for('library.list_publishers')) - form.name.data = publisher.name - return render_template('simpleform.html', action="Edit", - form=form, publisher=publisher, title="Edit Publisher") - - -@LIBRARY.route('/publishers/delete/', methods=['GET', 'POST']) -@login_required -def delete_publisher(publisher_id): - """ - Delete a publisher - :param publisher_id: ObjectId of publisher - :return: - """ - publisher = Publisher.objects.raw({'_id': ObjectId(publisher_id)}) - if publisher: - publisher.delete() - flash('You have successfully deleted the publisher.') - return redirect(url_for('library.list_publishers')) - - -@LIBRARY.route('/authors') -@login_required -def list_authors(): - """ - List all authors - :return: - """ - authors = Author.objects.all() - return render_template('library/authors.html', - authors=authors, title="Authors") - - -@LIBRARY.route('/authors/edit/', methods=['GET', 'POST']) -@login_required -def edit_author(author_id): - """ - Edit a book artist - """ - author = Author.objects.get({'_id': ObjectId(author_id)}) - form = AuthorForm(obj=author) - if form.validate_on_submit(): - author.name = form.name.data - author.save() - flash('You have successfully edited the author.') - return redirect(url_for('library.list_authors')) - form.name.data = author.name - return render_template('simpleform.html', action="Edit", - form=form, author=author, title="Edit Author") - - -@LIBRARY.route('/authors/add', methods=['GET', 'POST']) -@login_required -def add_author(): - """ - Add a author - :return: - """ - form = AuthorForm() - if form.validate_on_submit(): - author = Author() - author.name = form.name.data - try: - author.save() - flash('You have successfully added a new author.') - except PyMongoError: - flash('Error: author name already exists.') - return redirect(url_for('library.list_authors')) - return render_template('simpleform.html', action="Add", - form=form, title="Add Author") - - -@LIBRARY.route('/authors/delete/', methods=['GET', 'POST']) -@login_required -def delete_author(author_id): - """ - Delete a author - :param author_id: - :return: - """ - author = Author.objects.raw({'_id': ObjectId(author_id)}) - if author: - author.delete() - flash('You have successfully deleted the author.') - return redirect(url_for('library.list_authors')) - - -@LIBRARY.route('/books') -@login_required -def list_books(): - """ - List all comics - :return: - """ - books = Book.objects.all() - return render_template('library/books.html', - books=books, title="Books") - - -@LIBRARY.route('/books/edit/', methods=['GET', 'POST']) -@login_required -def edit_book(book_id): - """ - Edit a book - """ - book = Book.objects.get({'_id': ObjectId(book_id)}) - form = BookForm(obj=book) - form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] - if book.publisher: - form.publisher.default = book.publisher.pk - form.publisher.process_data(book.publisher.pk) - form.author.choices = [(a.pk, a.name) for a in Author.objects.all()] - if book.author: - form.author.default = book.author.pk - form.author.process_data(book.author.pk) - if form.validate_on_submit(): - book.title = form.title.data - book.publisher = form.publisher.data - book.author = form.author.data - if form.isbn.data: - book.isbn = form.isbn.data - else: - book.isbn = None - book.year = form.year.data - book.save() - flash('You have successfully edited the book.') - return redirect(url_for('library.list_books')) - form.title.data = book.title - return render_template('simpleform.html', action="Edit", - form=form, book=book, title="Edit Book") - - -@LIBRARY.route('/books/add', methods=['GET', 'POST']) -@login_required -def add_book(): - """ - Add a book - :return: - """ - form = BookForm() - form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] - form.author.choices = [(a.pk, a.name) for a in Author.objects.all()] - if form.validate_on_submit(): - book = Book() - book.title = form.title.data - book.publisher = form.publisher.data - book.author = form.author.data - book.year = form.year.data - try: - book.save() - flash('You have successfully added a new book.') - except PyMongoError: - flash('Error: book title already exists.') - return redirect(url_for('library.list_books')) - return render_template('simpleform.html', action="Add", - form=form, title="Add Book") - - -@LIBRARY.route('/books/delete/', methods=['GET', 'POST']) -@login_required -def delete_book(book_id): - """ - Delete a book - :param book_id: - :return: - """ - book = Book.objects.raw({'_id': ObjectId(book_id)}) - if book: - book.delete() - flash('You have successfully deleted the book.') - return redirect(url_for('library.list_books')) diff --git a/flask/kontor/main/__init__.py b/flask/kontor/main/__init__.py new file mode 100644 index 0000000..ebdaf6e --- /dev/null +++ b/flask/kontor/main/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +bp = Blueprint('main', __name__) + +from kontor.main import routes diff --git a/flask/kontor/main/routes.py b/flask/kontor/main/routes.py new file mode 100644 index 0000000..0c2e5dd --- /dev/null +++ b/flask/kontor/main/routes.py @@ -0,0 +1,18 @@ +from flask import render_template, session + +from kontor import app +from kontor.main import bp +from kontor.auth import auth + + +@bp.route('/') +@auth.login_required +def index(): + return render_template('index.html') + +@bp.route('/logout') +def logout(): + app.logger.info("logout") + auth.current_user() + session['user_name'] = None + return render_template('index.html') diff --git a/flask/kontor/media/__init__.py b/flask/kontor/media/__init__.py new file mode 100644 index 0000000..581af5f --- /dev/null +++ b/flask/kontor/media/__init__.py @@ -0,0 +1,9 @@ +from flask import Blueprint + +media_bp = Blueprint('media_bp', __name__, + template_folder='templates', + static_folder='static', static_url_path='assets') +media_api = Blueprint('media_api', __name__) + +from kontor.media import routes +from kontor.media import api diff --git a/flask/kontor/media/api.py b/flask/kontor/media/api.py new file mode 100644 index 0000000..be9b875 --- /dev/null +++ b/flask/kontor/media/api.py @@ -0,0 +1,18 @@ +from flask import Blueprint, render_template, jsonify + +from kontor import app +from kontor.media import media_api +from kontor.media.models import MediaFile, mediafile_schema, mediafiles_schema + + +@media_api.route('/') +def mediafile_list(): + app.logger.info("get all media files") + files = MediaFile.query.all() + return mediafiles_schema.dump(files) + + +@media_api.route('/mediafile/') +def mediafile_detail(file_id): + file = MediaFile.query.get(file_id) + return mediafile_schema.dump(file) diff --git a/flask/kontor/media/models.py b/flask/kontor/media/models.py new file mode 100644 index 0000000..4e54978 --- /dev/null +++ b/flask/kontor/media/models.py @@ -0,0 +1,43 @@ +from kontor.extensions import db, ma +from sqlalchemy.sql import func + + +class MediaFile(db.Model): + # __table__ = db.metadata.tables["publisher"] + __tablename__ = "media_file" + __table_args__ = {'extend_existing': True} + id = db.Column(db.String, primary_key=True) + created_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + last_modified_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + version = db.Column(db.Integer) + title = db.Column(db.String) + file_name = db.Column(db.String) + url = db.Column(db.String) + path = db.Column(db.String) + cloud_link = db.Column(db.String) + review = db.Column(db.SmallInteger) + should_download = db.Column(db.Boolean) + + def is_review(self): + return self.review == 'b\x00' + + def is_download(self): + return self.should_download == 'b\x00' + + +class MediaFileSchema(ma.SQLAlchemySchema): + class Meta: + model = MediaFile + fields = ("id", "created_date", "last_modified_date", "version", "title", "file_name", "url", "path", "cloud_link", "review", "should_download", "_links") + + # Smart hyperlinking + _links = ma.Hyperlinks( + { + "self": ma.URLFor("media_api.mediafile_detail", values=dict(mediafile_id="")), + "collection": ma.URLFor("media_api.mediafile_list"), + } + ) + + +mediafile_schema = MediaFileSchema() +mediafiles_schema = MediaFileSchema(many=True) diff --git a/flask/kontor/media/routes.py b/flask/kontor/media/routes.py new file mode 100644 index 0000000..a32f5e3 --- /dev/null +++ b/flask/kontor/media/routes.py @@ -0,0 +1,16 @@ +from flask import Blueprint, render_template + +from kontor.media import media_bp +from kontor.media.models import MediaFile + + +@media_bp.route('/') +def mediafile_list(): + files = MediaFile.query.all() + return render_template('media/mediafile_list.html', mediafiles=files) + + +@media_bp.route('/mediafile/') +def mediafile_detail(mediafile_id): + mediafile = MediaFile.query.get(mediafile_id) + return render_template('media/mediafile_detail.html', mediafile=mediafile) diff --git a/flask/kontor/media/templates/media/mediafile_detail.html b/flask/kontor/media/templates/media/mediafile_detail.html new file mode 100644 index 0000000..e3474ae --- /dev/null +++ b/flask/kontor/media/templates/media/mediafile_detail.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} + +{% block content %} + +

{% block title %} MediaFile {% endblock %}

+
+
+

MediaFile Blueprint

+
+ +

+ {{ mediafile.title }} +

+
+ +

{{ mediafile.id }}

+
+
+

Created

+

{{ mediafile.created_date }}

+

Modified

+

{{ mediafile.last_modified_date }}

+
+

Version: {{ mediafile.version }}

+

Review: {{ mediafile.is_review() }}

+

Should Download: {{mediafile.is_download() }}

+
+
+{% endblock %} diff --git a/flask/kontor/media/templates/media/mediafile_list.html b/flask/kontor/media/templates/media/mediafile_list.html new file mode 100644 index 0000000..2d7d29f --- /dev/null +++ b/flask/kontor/media/templates/media/mediafile_list.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} + +{% block content %} + +

{% block title %} MediaFile {% endblock %}

+
+
+

MediaFile Blueprint

+ {% for mediafile in mediafiles %} +
+ +

+ {{ mediafile.title }} +

+
+

{{ mediafile.id }}

+
+

Created

+

{{ mediafile.created_date }}

+

Modified

+

{{ mediafile.last_modified_date }}

+
+

Version: {{ mediafile.version }}

+

Review: {{ mediafile.is_review() }}

+

Should Download: {{ mediafile.is_download() }}

+

Links: {{ mediafile._links }}

+
+ {% endfor %} +
+{% endblock %} diff --git a/flask/kontor/models.py b/flask/kontor/models.py index 7150364..eba153c 100644 --- a/flask/kontor/models.py +++ b/flask/kontor/models.py @@ -1,66 +1,49 @@ -""" -This modules declares the model for Kontor users. -""" -from pymodm import MongoModel, fields -from pymongo.write_concern import WriteConcern -from flask_login import UserMixin -from werkzeug.security import generate_password_hash, check_password_hash -from kontor import _LOGIN_MANAGER_ +from kontor.extensions import db, ma +from sqlalchemy.sql import func -class User(UserMixin, MongoModel): - """ - This class represents an user for the Kontor application. - """ - email = fields.EmailField() - username = fields.CharField(max_length=60) - first_name = fields.CharField(max_length=60) - last_name = fields.CharField(max_length=60) - password_hash = fields.CharField(max_length=128) - is_admin = fields.BooleanField(default=False) +class Publisher(db.Model): + # __table__ = db.metadata.tables["publisher"] + __tablename__ = "publisher" + __table_args__ = {'extend_existing': True} + id = db.Column(db.String, primary_key=True) + created_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + last_modified_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + version = db.Column(db.Integer) + name = db.Column(db.String) + comics = db.relationship('Comic', back_populates='publisher', lazy='dynamic') - @property - def password(self): - """ - Prevent pasword from being accessed - """ - raise AttributeError('password is not a readable attribute.') - - @password.setter - def password(self, password): - """ - Set password to a hashed password - """ - self.password_hash = generate_password_hash(password) - - def verify_password(self, password): - """ - Check if hashed password matches actual password - """ - return check_password_hash(self.password_hash, password) - - def get_id(self): - """ - Get username as id - :return: - """ - return self.username - - def __repr__(self): - return ''.format(self.username) +class PublisherSchema(ma.SQLAlchemySchema): class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) + model = Publisher + + comics = ma.List(ma.HyperlinkRelated("comics_api.index")) -# Set up user_loader -@_LOGIN_MANAGER_.user_loader -def load_user(user_name): - """ - Get list of users from database - :param user_name: - :return: - """ - return User.objects.get({'username': user_name}) +publisher_schema = PublisherSchema() +publishers_schema = PublisherSchema(many=True) + + +class Comic(db.Model): + # __table__ = db.metadata.tables["comic"] + __tablename__ = "comic" + __table_args__ = {'extend_existing': True} + id = db.Column(db.String, primary_key=True) + created_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + last_modified_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + version = db.Column(db.Integer) + title = db.Column(db.String) + publisher_id = db.Column(db.String, db.ForeignKey("publisher.id")) + publisher = db.relationship("Publisher", back_populates="comics") + + +class ComicSchema(ma.SQLAlchemyAutoSchema): + class Meta: + model = Comic + include_fk = True + publisher = ma.HyperlinkRelated("comics_api.publisher_detail") + + +comic_schema = ComicSchema() +comics_schema = ComicSchema(many=True) diff --git a/flask/kontor/office/__init__.py b/flask/kontor/office/__init__.py deleted file mode 100644 index d830d41..0000000 --- a/flask/kontor/office/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Define routing rules for office related information -""" diff --git a/flask/kontor/static/css/style.css b/flask/kontor/static/css/style.css deleted file mode 100644 index 35fe053..0000000 --- a/flask/kontor/static/css/style.css +++ /dev/null @@ -1,126 +0,0 @@ -/* app/static/css/style.css */ - -body, html { - width: 100%; - height: 100%; -} - -body, h1, h2, h3 { - font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; - font-weight: 700; -} - -a, .navbar-default .navbar-brand, .navbar-default .navbar-nav>li>a { - color: #aec251; -} - -a:hover, .navbar-default .navbar-brand:hover, .navbar-default .navbar-nav>li>a:hover { - color: #687430; -} - -footer { - padding: 50px 0; - background-color: #f8f8f8; -} - -p.copyright { - margin: 15px 0 0; -} - -.alert-info { - width: 50%; - margin: auto; - color: #687430; - background-color: #e6ecca; - border-color: #aec251; -} - -.btn-default { - border-color: #aec251; - color: #aec251; -} - -.btn-default:hover { - background-color: #aec251; -} - -.center { - margin: auto; - width: 70%; - padding: 10px; -} - -.content-section { - padding: 50px 0; - border-top: 1px solid #e7e7e7; -} - -.footer, .push { - clear: both; - height: 4em; -} - -.intro-divider { - width: 400px; - border-top: 1px solid #f8f8f8; - border-bottom: 1px solid rgba(0,0,0,0.2); -} - -.intro-header { - padding-top: 50px; - padding-bottom: 50px; - text-align: center; - color: #f8f8f8; - background: url(../img/intro-bg.jpg) no-repeat center center; - background-size: cover; - height: 100%; -} - -.intro-message { - position: relative; - padding-top: 20%; - padding-bottom: 20%; -} - -.intro-message > h1 { - margin: 0; - text-shadow: 2px 2px 3px rgba(0,0,0,0.6); - font-size: 5em; -} - -.intro-message > h3 { - text-shadow: 2px 2px 3px rgba(0,0,0,0.6); -} - -.lead { - font-size: 18px; - font-weight: 400; -} - -.topnav { - font-size: 14px; -} - -.wrapper { - min-height: 100%; - height: auto !important; - height: 100%; - margin: 0 auto -4em; -} - -.outer { - display: table; - position: absolute; - height: 70%; - width: 100%; -} - -.middle { - display: table-cell; - vertical-align: middle; -} - -.inner { - margin-left: auto; - margin-right: auto; -} diff --git a/flask/kontor/templates/admin/users/user.html b/flask/kontor/templates/admin/users/user.html deleted file mode 100644 index 50df2c4..0000000 --- a/flask/kontor/templates/admin/users/user.html +++ /dev/null @@ -1,21 +0,0 @@ - - -{% import "bootstrap/wtf.html" as wtf %} -{% extends "base.html" %} -{% block title %}Edit User{% endblock %} -{% block body %} -
-
-
-
-
-

Edit User

-
-
- {{ wtf.quick_form(form) }} -
-
-
-
-
-{% endblock %} diff --git a/flask/kontor/templates/admin/users/users.html b/flask/kontor/templates/admin/users/users.html deleted file mode 100644 index b23f67c..0000000 --- a/flask/kontor/templates/admin/users/users.html +++ /dev/null @@ -1,58 +0,0 @@ - - -{% import "bootstrap/utils.html" as utils %} -{% extends "base.html" %} -{% block title %}Users{% endblock %} -{% block body %} -
-
-
-
-
- {{ utils.flashed_messages() }} -
-

Users

- {% if users %} -
-
- - - - - - - - - - - {% for user in users %} - {% if user.is_admin %} - - - - - - - {% else %} - - - - - - - {% endif %} - {% endfor %} - -
Name Email Username Edit
Admin N/A N/A N/A
{{ user.first_name }} {{ user.last_name }} {{ user.email }} {{ user.username }} - - Edit - -
-
- {% endif %} -
-
-
-
-
-{% endblock %} diff --git a/flask/kontor/templates/auth/login.html b/flask/kontor/templates/auth/login.html deleted file mode 100644 index 8b70e01..0000000 --- a/flask/kontor/templates/auth/login.html +++ /dev/null @@ -1,18 +0,0 @@ - - -{% import "bootstrap/utils.html" as utils %} -{% import "bootstrap/wtf.html" as wtf %} -{% extends "base.html" %} -{% block title %}Login{% endblock %} -{% block body %} -
-
- {{ utils.flashed_messages() }} -
-
-

Login to your account

-
- {{ wtf.quick_form(form) }} -
-
-{% endblock %} diff --git a/flask/kontor/templates/auth/register.html b/flask/kontor/templates/auth/register.html deleted file mode 100644 index 55d4918..0000000 --- a/flask/kontor/templates/auth/register.html +++ /dev/null @@ -1,14 +0,0 @@ - - -{% import "bootstrap/wtf.html" as wtf %} -{% extends "base.html" %} -{% block title %}Register{% endblock %} -{% block body %} -
-
-

Register for an account

-
- {{ wtf.quick_form(form) }} -
-
-{% endblock %} diff --git a/flask/kontor/templates/base.html b/flask/kontor/templates/base.html index 80bdc27..0d7af2d 100644 --- a/flask/kontor/templates/base.html +++ b/flask/kontor/templates/base.html @@ -1,107 +1,68 @@ - - {{ title }} | Kontor - - - - - + + {% block title %} {% endblock %} - FlaskApp + - + + + diff --git a/go/templates/kontor/header.html b/go/templates/kontor/header.html new file mode 100644 index 0000000..bcf20aa --- /dev/null +++ b/go/templates/kontor/header.html @@ -0,0 +1,17 @@ + + + + + + + {{ .title }} + + + + + + + + + + diff --git a/go/templates/kontor/index.html b/go/templates/kontor/index.html new file mode 100644 index 0000000..4e23ba7 --- /dev/null +++ b/go/templates/kontor/index.html @@ -0,0 +1,17 @@ + +{{ define "kontor/index.html" }} + +{{ template "header.html" .}} + +{{ template "menu.html" . }} + +
+ {{ if .InfoMessage}} +

{{.InfoMessage}}

+ {{end}} +
+ + +{{ template "footer.html" .}} + +{{ end }} diff --git a/go/templates/kontor/login-successful.html b/go/templates/kontor/login-successful.html new file mode 100644 index 0000000..46e07fa --- /dev/null +++ b/go/templates/kontor/login-successful.html @@ -0,0 +1,12 @@ + + + +{{ template "header.html" .}} +{{ template "menu.html" . }} + +
+ You have successfully logged in. +
+ + +{{ template "footer.html" .}} diff --git a/go/templates/kontor/login.html b/go/templates/kontor/login.html new file mode 100644 index 0000000..83fce69 --- /dev/null +++ b/go/templates/kontor/login.html @@ -0,0 +1,35 @@ + + + +{{ template "header.html" .}} +{{ template "menu.html" . }} + +

Login

+ + +
+
+ + {{ if .ErrorTitle}} +

+ {{.ErrorTitle}}: {{.ErrorMessage}} +

+ {{end}} + +
+
+ + +
+
+ + +
+ +
+
+
+ + + +{{ template "footer.html" .}} diff --git a/go/templates/kontor/menu.html b/go/templates/kontor/menu.html new file mode 100644 index 0000000..b52a358 --- /dev/null +++ b/go/templates/kontor/menu.html @@ -0,0 +1,36 @@ + diff --git a/go/templates/kontor/user-detail.html b/go/templates/kontor/user-detail.html new file mode 100644 index 0000000..983a614 --- /dev/null +++ b/go/templates/kontor/user-detail.html @@ -0,0 +1,59 @@ +{{ define "kontor/user-detail.html" }} +{{ template "header.html" .}} +{{ template "admin-menu.html" .}} + + +
+ + {{ if .ErrorTitle}} +

+ {{.ErrorTitle}}: {{.ErrorMessage}} +

+ {{end}} +
+
+ + {{ if .payload.Username }} + + {{ else }} + + {{ end }} +
+
+
+ + {{ if .payload.Firstname }} + + {{ else }} + + {{ end }} +
+
+ + {{ if .payload.Lastname }} + + {{ else }} + + {{ end }} +
+
+
+ + +
+
+ + +
+ + + {{ if eq .action "Save" }} + + {{ end }} +
+
+{{ template "footer.html" .}} +{{end}} diff --git a/go/templates/kontor/users.html b/go/templates/kontor/users.html new file mode 100644 index 0000000..8aef51c --- /dev/null +++ b/go/templates/kontor/users.html @@ -0,0 +1,38 @@ +{{ define "kontor/users.html" }} +{{ template "header.html" .}} +{{ template "admin-menu.html" .}} + + +
+ + + + + + + + + {{range .payload }} + + + + + {{ if .IsAdmin}} + + {{end}} + {{ if not .IsAdmin }} + + {{end}} + + {{end}} +
Registered users of Kontor
UsernameFirst NameLast NameAdministrator
{{.Username}}{{.Firstname}}{{.Lastname}}
+
+ Add entry +
+
+ +{{ template "footer.html" .}} +{{end}} diff --git a/go/templates/library/authors.html b/go/templates/library/authors.html new file mode 100644 index 0000000..781ec26 --- /dev/null +++ b/go/templates/library/authors.html @@ -0,0 +1,25 @@ + +{{ define "library/authors.html" }} +{{ template "header.html" .}} +{{ template "library/menu.html" .}} + +
+ + + + {{range .payload }} + + {{end}} +
Liste der Autoren
Name
{{.Name}}
+
+ Add entry +
+
+ + +{{ template "footer.html" .}} +{{ end }} diff --git a/go/templates/library/books.html b/go/templates/library/books.html new file mode 100644 index 0000000..456cb82 --- /dev/null +++ b/go/templates/library/books.html @@ -0,0 +1,25 @@ + +{{ define "library/books.html" }} +{{ template "header.html" .}} +{{ template "library/menu.html" . }} + +
+ + + + {{range .payload }} + + {{end}} +
Liste der Bücher
Title
{{.Title}}
+
+ Add entry +
+
+ +{{ template "footer.html" .}} + +{{ end }} diff --git a/go/templates/library/menu.html b/go/templates/library/menu.html new file mode 100644 index 0000000..2e6257f --- /dev/null +++ b/go/templates/library/menu.html @@ -0,0 +1,38 @@ +{{ define "library/menu.html" }} + +{{ end }} diff --git a/go/templates/library/publisher.html b/go/templates/library/publisher.html new file mode 100644 index 0000000..c0dae5e --- /dev/null +++ b/go/templates/library/publisher.html @@ -0,0 +1,32 @@ +{{ define "library/publisher.html" }} +{{ template "header.html" .}} +{{ template "library/menu.html" .}} + +
+ {{ if .ErrorTitle}} +

+ {{.ErrorTitle}}: {{.ErrorMessage}} +

+ {{end}} +
+
+ + {{ if .payload.Name }} + + {{ else }} + + {{ end }} +
+ + + {{ if eq .action "Save" }} + + {{ end }} +
+
+{{ template "footer.html" .}} +{{ end }} diff --git a/go/templates/library/publishers.html b/go/templates/library/publishers.html new file mode 100644 index 0000000..a166167 --- /dev/null +++ b/go/templates/library/publishers.html @@ -0,0 +1,24 @@ + +{{ define "library/publishers.html" }} +{{ template "header.html" .}} +{{ template "library/menu.html" .}} + +
+ + + + {{range .payload }} + + {{end}} +
Liste der Verlage
Name
{{.Name}}
+
+ Add entry +
+
+ +{{ template "footer.html" .}} +{{end}} diff --git a/go/templates/office/index.html b/go/templates/office/index.html new file mode 100644 index 0000000..3932a29 --- /dev/null +++ b/go/templates/office/index.html @@ -0,0 +1,10 @@ + +{{ define "office/index.html" }} +{{ template "header.html" .}} +{{ template "office/menu.html" . }} + +

Home Office

+ +{{ template "footer.html" .}} + +{{ end }} diff --git a/go/templates/office/menu.html b/go/templates/office/menu.html new file mode 100644 index 0000000..ddcd03d --- /dev/null +++ b/go/templates/office/menu.html @@ -0,0 +1,38 @@ +{{ define "office/menu.html" }} + +{{ end }} diff --git a/go/templates/tradingcards/index.html b/go/templates/tradingcards/index.html new file mode 100644 index 0000000..6911e17 --- /dev/null +++ b/go/templates/tradingcards/index.html @@ -0,0 +1,10 @@ + +{{ define "tradingcards/index.html" }} +{{ template "header.html" .}} +{{ template "tradingcards/menu.html" . }} + +

Trading Cards

+ +{{ template "footer.html" .}} + +{{ end }} diff --git a/go/templates/tradingcards/menu.html b/go/templates/tradingcards/menu.html new file mode 100644 index 0000000..524080c --- /dev/null +++ b/go/templates/tradingcards/menu.html @@ -0,0 +1,38 @@ +{{ define "tradingcards/menu.html" }} + +{{ end }} diff --git a/go/templates/tysc/index.html b/go/templates/tysc/index.html new file mode 100644 index 0000000..b1d135d --- /dev/null +++ b/go/templates/tysc/index.html @@ -0,0 +1,16 @@ +{{ define "tysc/index.html" }} +{{ template "header.html" .}} +{{ template "tysc/menu.html" . }} + +{{ template "footer.html" .}} +{{ end }} diff --git a/go/templates/tysc/menu.html b/go/templates/tysc/menu.html new file mode 100644 index 0000000..53a8e9e --- /dev/null +++ b/go/templates/tysc/menu.html @@ -0,0 +1,38 @@ +{{ define "tysc/menu.html" }} + +{{ end }} diff --git a/go/templates/tysc/sports.html b/go/templates/tysc/sports.html new file mode 100644 index 0000000..cf65f25 --- /dev/null +++ b/go/templates/tysc/sports.html @@ -0,0 +1,28 @@ +{{ define "tysc/sports.html" }} +{{ template "header.html" .}} +{{ template "tysc/menu.html" .}} + +
+ + + + {{range .payload }} + + {{end}} +
List of Amertican Sports
Name
{{.Name}}
+
+ Add entry +
+
+{{ template "footer.html" .}} +{{ end }} diff --git a/go/tysc-20041010-1819.sql b/go/tysc-20041010-1819.sql new file mode 100644 index 0000000..167c41d --- /dev/null +++ b/go/tysc-20041010-1819.sql @@ -0,0 +1,168 @@ +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE=NO_AUTO_VALUE_ON_ZERO */; +CREATE DATABASE /*!32312 IF NOT EXISTS*/ `tysc`; +USE `tysc`; + +DROP TABLE IF EXISTS `angebote`; +CREATE TABLE `angebote` ( + `user_id` int(11) NOT NULL default '0', + `karte_id` int(11) NOT NULL default '0' +) TYPE=MyISAM; +INSERT INTO `angebote` (`user_id`,`karte_id`) VALUES (3,28),(3,30); + +DROP TABLE IF EXISTS `benutzer`; +CREATE TABLE `benutzer` ( + `ID` int(11) NOT NULL auto_increment, + `forename` varchar(40) default NULL, + `surname` varchar(40) default NULL, + `strasse` varchar(60) default NULL, + `plz` int(6) default NULL, + `ort` varchar(20) default NULL, + `username` varchar(20) default NULL, + `email` varchar(60) default NULL, + `password` varchar(32) default NULL, + `java` char(1) default NULL, + `language` int(11) NOT NULL default '0', + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `benutzer` (`ID`,`forename`,`surname`,`strasse`,`plz`,`ort`,`username`,`email`,`password`,`java`,`language`) VALUES (1,'Thomas','Peetz','Reichweindamm 24',13627,'Berlin','gophard','thomas.peetz@snafu.de','t.log1n','N',1),(2,'Heiko','John','Johannastr.49',13581,'Berlin','John','healjo@hotmail.com','redskins','N',1),(3,'Thomas','Peetz','Reichweindamm 24',13627,'Berlin','peetz','gophard@snafu.de','peetz','N',1); + +DROP TABLE IF EXISTS `changelog`; +CREATE TABLE `changelog` ( + `datum` date default NULL, + `tablename` varchar(20) default NULL, + `id` int(11) NOT NULL default '0' +) TYPE=MyISAM; +INSERT INTO `changelog` (`datum`,`tablename`,`id`) VALUES ('2002-02-27','benutzer',1),('2002-02-28','benutzer',2),('2002-03-05','benutzer',3),('2002-03-05','angebote',0),('2002-03-05','angebote',0); + +DROP TABLE IF EXISTS `hersteller`; +CREATE TABLE `hersteller` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(30) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `hersteller` (`ID`,`name`) VALUES (1,'Pacific'),(2,'Fleer'),(3,'Bowman'),(6,'Topps'),(7,'Donruss'),(8,'Score'),(9,'Flair'); + +DROP TABLE IF EXISTS `inserts`; +CREATE TABLE `inserts` ( + `ID` int(11) NOT NULL auto_increment, + `hersteller_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`,`hersteller_id`) +) TYPE=MyISAM; +INSERT INTO `inserts` (`ID`,`hersteller_id`,`name`) VALUES (1,2,'Mystique Big Buzz'); + +DROP TABLE IF EXISTS `karte`; +CREATE TABLE `karte` ( + `ID` int(11) NOT NULL auto_increment, + `spieler_id` int(11) NOT NULL default '0', + `team_id` int(11) NOT NULL default '0', + `hersteller_id` int(11) NOT NULL default '0', + `serie_id` int(11) default NULL, + `parallel_id` int(11) default NULL, + `inserts_id` int(11) default NULL, + `rookie` char(1) default NULL, + `jahr` int(4) default NULL, + `nummer` int(11) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `karte` (`ID`,`spieler_id`,`team_id`,`hersteller_id`,`serie_id`,`parallel_id`,`inserts_id`,`rookie`,`jahr`,`nummer`) VALUES (12,12,13,1,1,0,0,'N',2001,212),(1,1,2,1,1,0,0,'N',2001,185),(2,2,2,1,1,0,0,'N',2001,250),(3,3,8,1,1,0,0,'N',2001,103),(4,4,8,1,1,0,0,'N',2001,112),(5,5,6,1,1,0,0,'N',2001,37),(6,6,6,1,1,0,0,'N',2001,38),(7,7,6,1,1,0,0,'N',2001,31),(8,8,10,1,1,0,0,'N',2001,338),(9,9,10,1,1,0,0,'N',2001,335),(10,10,10,1,1,0,0,'N',2001,345),(11,11,13,1,1,0,0,'N',2001,213),(13,13,14,1,1,0,0,'N',2001,311),(14,14,14,1,1,0,0,'N',2001,312),(15,15,16,1,1,0,0,'N',2001,403),(16,16,16,1,1,0,0,'N',2001,397),(17,17,16,1,1,0,0,'N',2001,404),(18,18,18,1,1,0,0,'N',2001,116),(19,19,18,1,1,0,0,'N',2001,122),(20,20,18,1,1,0,0,'N',2001,117),(21,21,19,1,1,0,0,'N',2001,281),(22,22,19,1,1,0,0,'N',2001,321),(23,23,20,1,1,0,0,'N',2001,331),(24,24,20,1,1,0,0,'N',2001,324),(25,25,21,1,1,0,0,'N',2001,445),(26,26,27,1,1,0,0,'N',2001,28),(27,27,27,1,1,0,0,'N',2001,17),(28,28,27,1,1,0,0,'N',2001,23),(29,29,29,1,1,0,0,'N',2001,273); +INSERT INTO `karte` (`ID`,`spieler_id`,`team_id`,`hersteller_id`,`serie_id`,`parallel_id`,`inserts_id`,`rookie`,`jahr`,`nummer`) VALUES (30,30,31,1,1,0,0,'N',2001,380),(31,31,31,1,1,0,0,'N',2001,390),(32,32,31,1,1,0,0,'N',2001,381),(33,33,31,1,1,0,0,'N',2001,387),(34,34,31,1,1,0,0,'N',2001,386),(35,35,30,1,1,0,0,'N',2001,349),(36,36,30,1,1,0,0,'N',2001,350),(37,37,44,5,8,0,0,'N',1994,106); + +DROP TABLE IF EXISTS `language`; +CREATE TABLE `language` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(15) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `mannschaft`; +CREATE TABLE `mannschaft` ( + `ID` int(11) NOT NULL auto_increment, + `team_id` int(11) NOT NULL default '0', + `sportart_id` int(11) NOT NULL default '0', + PRIMARY KEY (`ID`) +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `parallelset`; +CREATE TABLE `parallelset` ( + `ID` int(11) NOT NULL auto_increment, + `hersteller_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`,`hersteller_id`) +) TYPE=MyISAM; +INSERT INTO `parallelset` (`ID`,`hersteller_id`,`name`) VALUES (1,2,'Mystique Gold'),(2,1,'Pacific Copper'),(3,1,'Pacific Gold'); + +DROP TABLE IF EXISTS `position`; +CREATE TABLE `position` ( + `ID` int(11) NOT NULL auto_increment, + `sportart_id` int(11) NOT NULL default '0', + `name` varchar(20) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `position` (`ID`,`sportart_id`,`name`) VALUES (1,1,'QB'),(2,1,'WR'),(3,1,'RB'),(4,1,'LB'),(5,1,'TE'),(6,1,'FB'),(7,1,'SS'),(8,1,'DE'),(9,1,'K'),(10,1,'P'),(11,1,'LG'),(12,1,'RG'),(13,1,'OF'),(14,1,'DB'),(15,1,'CB'),(16,2,'C'),(17,2,'1B'),(18,2,'2B'),(19,2,'3B'),(20,2,'SS'),(21,2,'LF'),(22,2,'CF'),(23,2,'RF'),(24,2,'DH'),(25,2,'P'); + +DROP TABLE IF EXISTS `serie`; +CREATE TABLE `serie` ( + `ID` int(11) NOT NULL auto_increment, + `hersteller_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`,`hersteller_id`) +) TYPE=MyISAM; +INSERT INTO `serie` (`ID`,`hersteller_id`,`name`) VALUES (1,1,'Pacific'),(2,2,'Fleer'),(3,3,'Bowman'),(4,4,'Leaf'),(5,2,'Ultra'),(6,2,'Mystique'),(7,1,'Finest Hour'),(8,5,'SP'),(9,5,'SPX'),(10,5,'SP Authentic'),(11,5,'Black Diamond'); + +DROP TABLE IF EXISTS `spiele`; +CREATE TABLE `spiele` ( + `datum` date default NULL, + `gast` int(11) NOT NULL default '0', + `gast_pkt` int(11) default NULL, + `heim` int(11) NOT NULL default '0', + `heim_pkt` int(11) default NULL +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `spieler`; +CREATE TABLE `spieler` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `spieler` (`ID`,`name`) VALUES (1,'Pathon, Jerome'),(2,'Bruschi, Tedy'),(3,'Couch, Tim'),(4,'Shea, Aaron'),(5,'Lewis, Jamal'),(6,'Lewis, Jermaine'),(7,'Banks, Tony'),(8,'Fuamatu-Ma\'Afala, Chris'),(9,'Bettis, Jerome'),(10,'Stewart, Kordell'),(11,'Moon, Warren'),(12,'Lockett, Kevin'),(13,'Gannon, Rich'),(14,'Jett, James'),(15,'Strong, Mack'),(16,'Huard, Brock'),(17,'Watters, Ricky'),(18,'Aikman, Troy'),(19,'LaFleur, David'),(20,'Brazzell, Chris'),(21,'Dayne, Ron'),(22,'Brown, Na'),(23,'Small, Torrance'),(24,'Lewis, Chad'),(25,'Murrell, Adrian'),(26,'Smith, Maurice'),(27,'Chandler, Chris'),(28,'Kanell, Danny'),(29,'Williams, Ricky'),(30,'Garcia, Jeff'),(31,'Streets, Tai'),(32,'Garner, Charlie'),(33,'Rice, Jerry'),(34,'Owens, Terrell'),(35,'Bruce, Isaac'),(36,'Canidate, Trung'); + +DROP TABLE IF EXISTS `spielerposition`; +CREATE TABLE `spielerposition` ( + `spieler_id` int(11) NOT NULL default '0', + `sportart_id` int(11) NOT NULL default '0', + `position_id` int(11) NOT NULL default '0' +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `sportart`; +CREATE TABLE `sportart` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(30) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `sportart` (`ID`,`name`) VALUES (1,'Football'),(2,'Baseball'),(3,'Basketball'),(4,'Hockey'); + +DROP TABLE IF EXISTS `suche`; +CREATE TABLE `suche` ( + `user_id` int(11) NOT NULL default '0', + `karte_id` int(11) NOT NULL default '0' +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `team`; +CREATE TABLE `team` ( + `ID` int(11) NOT NULL auto_increment, + `sportart_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + `short` varchar(15) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (1,1,'Buffalo Bills','Bills'),(2,1,'Indianapolis Colts','Colts'),(3,1,'Miami Dolphins','Dolphins'),(4,1,'New England Patriots','Patriots'),(5,1,'New York Jets','Jets'),(6,1,'Baltimore Ravens','Ravens'),(7,1,'Cincinnati Bengals','Bengals'),(8,1,'Cleveland Browns','Browns'),(9,1,'Jacksonville Jaguars','Jaguars'),(10,1,'Pittsburgh Steelers','Steelers'),(11,1,'Tennessee Titans','Titans'),(12,1,'Denver Broncos','Broncos'),(13,1,'Kansas City Chiefs','Chiefs'),(14,1,'Oakland Raiders','Raiders'),(15,1,'San Diego Chargers','Chargers'),(16,1,'Seattle Seahawks','Seahawks'),(17,1,'Arizona Cardinals','Cardinals'),(18,1,'Dallas Cowboys','Cowboys'),(19,1,'New York Giants','Giants'),(20,1,'Philadelphia Eagles','Eagles'),(21,1,'Washington Redskins','Redskins'),(22,1,'Chicago Bears','Bears'),(23,1,'Detroit Lions','Lions'),(24,1,'Green Bay Packers','Packers'),(25,1,'Minnesota Vikings','Vikings'),(26,1,'Tampa Bay Buccaneers','Buccaneers'),(27,1,'Atlanta Falcons','Falcons'),(28,1,'Carolina Panthers','Panthers'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (29,1,'New Orleans Saints','Saints'),(30,1,'St.Louis Rams','Rams'),(31,1,'San Francisco 49ers','49ers'),(32,2,'Baltimore Orioles','Orioles'),(33,2,'Boston Red Sox','Red Sox'),(34,2,'New York Yankees','Yankees'),(35,2,'Tampa Bay Devil Rays','Devil Rays'),(36,2,'Toronto Blue Jays','Blue Jays'),(37,2,'Chicago White Sox','White Sox'),(38,2,'Cleveland Indians','Indians'),(39,2,'Detroit Tigers','Tigers'),(40,2,'Kansas City Royals','Royals'),(41,2,'Minnesota Twins','Twins'),(42,2,'Anaheim Angels','Angels'),(43,2,'Oakland Athletics','Athletics'),(44,2,'Seattle Mariners','Mariners'),(45,2,'Texas Rangers','Rangers'),(46,2,'Atlanta Braves','Braves'),(47,2,'Florida Marlins','Marlins'),(48,2,'Montreal Expos','Expos'),(49,2,'New York Mets','Mets'),(50,2,'Philadelphia Phillies','Phillies'),(51,2,'Chicago Cubs','Cubs'),(52,2,'Cincinnati Reds','Reds'),(53,2,'Houston Astros','Astros'),(54,2,'Milwaukee Brewers','Brewers'),(55,2,'Pittsburgh Pirates','Pirates'),(56,2,'St.Louis Cardinals','Cardinals'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (57,2,'Arizona Diamondbacks','Diamondbacks'),(58,2,'Colorado Rockies','Rockies'),(59,2,'Los Angeles Dodgers','Dodgers'),(60,2,'San Diego Padres','Padres'),(61,2,'San Francisco Giants','Giants'),(62,3,'Boston Celtics','Celtics'),(63,3,'Miami Heat','Heat'),(64,3,'New Jersey Nets','Mets'),(65,3,'New York Knicks','Knicks'),(66,3,'Orlando Magic','Magic'),(67,3,'Philadelphia 76ers','76ers'),(68,3,'Washington Wizards','Wizards'),(69,3,'Atlanta Hawks','Hawks'),(70,3,'Charlotte Hornets','Hornets'),(71,3,'Chicago Bulls','Bulls'),(72,3,'Cleveland Cavaliers','Cavaliers'),(73,3,'Detroit Pistons','Pistons'),(74,3,'Indiana Pacers','Pacers'),(75,3,'Milwaukee Bucks','Bucks'),(76,3,'Toronto Raptors','Raptors'),(77,3,'Dallas Mavericks','Mavericks'),(78,3,'Denver Nuggets','Nuggets'),(79,3,'Houston Rockets','Rockets'),(80,3,'Minnesota Timberwolves','Timberwolves'),(81,3,'San Antonio Spurs','Spurs'),(82,3,'Utah Jazz','Jazz'),(83,3,'Vancouver Grizzlies','Grizzlies'),(84,3,'Golden State Warriors','Warriors'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (85,3,'Los Angeles Clippers','Clippers'),(86,3,'Los Angeles Lakers','Lakers'),(87,3,'Phoenix Suns','Suns'),(88,3,'Portland Trail Blazers','Blazers'),(89,3,'Sacramento Kings','Kings'),(90,3,'Seattle SuperSonics','SuperSonics'),(91,4,'Boston Bruins','Bruins'),(92,4,'Buffalo Sabres','Sabres'),(93,4,'Montreal Canadiens','Canadiens'),(94,4,'Ottawa Senators','Senators'),(95,4,'Toronto Maple Leafs','Maple Leafs'),(96,4,'New Jersey Devils','Devils'),(97,4,'New York Islander','Islander'),(98,4,'New York Rangers','Rangers'),(99,4,'Philadelphia Flyers','Flyers'),(100,4,'Pittsburgh Penguins','Penguins'),(101,4,'Atlanta Trashers','Trashers'),(102,4,'Carolina Hurricanes','Hurricanes'),(103,4,'Florida Panthers','Panthers'),(104,4,'Tampa Bay Lightnings','Lightnings'),(105,4,'Washington Capitals','Capitals'),(106,4,'Chicago Blackhawks','Blackhawks'),(107,4,'Columbo Blue Jackets','Blue Jackets'),(108,4,'Detroit Red Wings','Red Wings'),(109,4,'Nashville Predators','Predators'),(110,4,'St.Louis Blues','Blues'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (111,4,'Calgary Flames','Flames'),(112,4,'Colorado Avalanche','Avalanche'),(113,4,'Edmonton Oilers','Oilers'),(114,4,'Minnesota Wild','Wild'),(115,4,'Vancouver Canucks','Canucks'),(116,4,'Anaheim Mighty Ducks','Mighty Ducks'),(117,4,'Dallas Stars','Stars'),(118,4,'Los Angeles Kings','Kings'),(119,4,'Phoenix Coyotes','Coyotes'),(120,4,'San Jose Sharks','Sharks'),(121,1,'Houston Texans','Texans'),(122,1,'Houston Oilers','Oilers'); +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; diff --git a/kotlin-quarkus/.dockerignore b/kotlin-quarkus/.dockerignore new file mode 100644 index 0000000..4361d2f --- /dev/null +++ b/kotlin-quarkus/.dockerignore @@ -0,0 +1,5 @@ +* +!build/*-runner +!build/*-runner.jar +!build/lib/* +!build/quarkus-app/* \ No newline at end of file diff --git a/kotlin-quarkus/.gitignore b/kotlin-quarkus/.gitignore new file mode 100644 index 0000000..285b6ba --- /dev/null +++ b/kotlin-quarkus/.gitignore @@ -0,0 +1,36 @@ +# Gradle +.gradle/ +build/ + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env diff --git a/kotlin-quarkus/README.md b/kotlin-quarkus/README.md new file mode 100644 index 0000000..08f2a76 --- /dev/null +++ b/kotlin-quarkus/README.md @@ -0,0 +1,78 @@ +# kontor Project + +This project uses Quarkus, the Supersonic Subatomic Java Framework. + +If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ . + +## Running the application in dev mode + +You can run your application in dev mode that enables live coding using: +```shell script +./gradlew quarkusDev +``` + +> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. + +## Packaging and running the application + +The application can be packaged using: +```shell script +./gradlew build +``` +It produces the `quarkus-run.jar` file in the `build/quarkus-app/` directory. +Be aware that it’s not an _über-jar_ as the dependencies are copied into the `build/quarkus-app/lib/` directory. + +The application is now runnable using `java -jar build/quarkus-app/quarkus-run.jar`. + +If you want to build an _über-jar_, execute the following command: +```shell script +./gradlew build -Dquarkus.package.type=uber-jar +``` + +The application, packaged as an _über-jar_, is now runnable using `java -jar build/*-runner.jar`. + +## Creating a native executable + +You can create a native executable using: +```shell script +./gradlew build -Dquarkus.package.type=native +``` + +Or, if you don't have GraalVM installed, you can run the native executable build in a container using: +```shell script +./gradlew build -Dquarkus.package.type=native -Dquarkus.native.container-build=true +``` + +You can then execute your native executable with: `./build/kontor-1.0.0-SNAPSHOT-runner` + +If you want to learn more about building native executables, please consult https://quarkus.io/guides/gradle-tooling. + +## Related Guides + +- JDBC Driver - H2 ([guide](https://quarkus.io/guides/datasource)): Connect to the H2 database via JDBC +- Hibernate ORM with Panache and Kotlin ([guide](https://quarkus.io/guides/hibernate-orm-panache-kotlin)): Define your persistent model in Hibernate ORM with Panache +- SmallRye OpenAPI ([guide](https://quarkus.io/guides/openapi-swaggerui)): Document your REST APIs with OpenAPI - comes with Swagger UI +- YAML Configuration ([guide](https://quarkus.io/guides/config#yaml)): Use YAML to configure your Quarkus application +- SmallRye Health ([guide](https://quarkus.io/guides/microprofile-health)): Monitor service health + +## Provided Code + +### YAML Config + +Configure your application with YAML + +[Related guide section...](https://quarkus.io/guides/config-reference#configuration-examples) + +The Quarkus application configuration is located in `src/main/resources/application.yml`. + +### RESTEasy Reactive + +Easily start your Reactive RESTful Web Services + +[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources) + +### SmallRye Health + +Monitor your application's health using SmallRye Health + +[Related guide section...](https://quarkus.io/guides/smallrye-health) diff --git a/kotlin-quarkus/build.gradle.kts b/kotlin-quarkus/build.gradle.kts new file mode 100644 index 0000000..0a6ac3a --- /dev/null +++ b/kotlin-quarkus/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + java + id("io.quarkus") +} + +repositories { + mavenCentral() + mavenLocal() +} + +val quarkusPlatformGroupId: String by project +val quarkusPlatformArtifactId: String by project +val quarkusPlatformVersion: String by project + +dependencies { + implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")) + implementation("io.quarkus:quarkus-resteasy-reactive-jackson") + implementation("io.quarkus:quarkus-jdbc-h2") + implementation("io.quarkus:quarkus-hibernate-orm-panache-kotlin") + implementation("io.quarkus:quarkus-smallrye-openapi") + implementation("io.quarkus:quarkus-config-yaml") + implementation("io.quarkus:quarkus-hibernate-reactive-panache-kotlin") + implementation("io.quarkus:quarkus-smallrye-health") + implementation("io.quarkus:quarkus-arc") + implementation("io.quarkus:quarkus-resteasy-reactive") + testImplementation("io.quarkus:quarkus-junit5") + testImplementation("io.rest-assured:rest-assured") +} + +group = "de.thpeetz.kontor" +version = "1.0.0-SNAPSHOT" + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +tasks.withType { + systemProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager") +} + +tasks.withType { + options.encoding = "UTF-8" + options.compilerArgs.add("-parameters") +} diff --git a/kotlin-quarkus/gradle.properties b/kotlin-quarkus/gradle.properties new file mode 100644 index 0000000..1057e5c --- /dev/null +++ b/kotlin-quarkus/gradle.properties @@ -0,0 +1,6 @@ +#Gradle properties +quarkusPluginId=io.quarkus +quarkusPluginVersion=2.15.2.Final +quarkusPlatformGroupId=io.quarkus.platform +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformVersion=2.15.2.Final \ No newline at end of file diff --git a/kotlin-quarkus/gradle/wrapper/gradle-wrapper.jar b/kotlin-quarkus/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..62d4c053550b91381bbd28b1afc82d634bf73a8a GIT binary patch literal 58910 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 literal 0 HcmV?d00001 diff --git a/kotlin-quarkus/gradle/wrapper/gradle-wrapper.properties b/kotlin-quarkus/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ae04661 --- /dev/null +++ b/kotlin-quarkus/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/kotlin-quarkus/gradlew b/kotlin-quarkus/gradlew new file mode 100755 index 0000000..fbd7c51 --- /dev/null +++ b/kotlin-quarkus/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/kotlin-quarkus/gradlew.bat b/kotlin-quarkus/gradlew.bat new file mode 100644 index 0000000..a9f778a --- /dev/null +++ b/kotlin-quarkus/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kotlin-quarkus/settings.gradle.kts b/kotlin-quarkus/settings.gradle.kts new file mode 100644 index 0000000..043f9fc --- /dev/null +++ b/kotlin-quarkus/settings.gradle.kts @@ -0,0 +1,13 @@ +pluginManagement { + val quarkusPluginVersion: String by settings + val quarkusPluginId: String by settings + repositories { + mavenCentral() + gradlePluginPortal() + mavenLocal() + } + plugins { + id(quarkusPluginId) version quarkusPluginVersion + } +} +rootProject.name="kontor" diff --git a/kotlin-quarkus/src/main/docker/Dockerfile.jvm b/kotlin-quarkus/src/main/docker/Dockerfile.jvm new file mode 100644 index 0000000..669d901 --- /dev/null +++ b/kotlin-quarkus/src/main/docker/Dockerfile.jvm @@ -0,0 +1,94 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./gradlew build +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/kontor-jvm . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-jvm +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-jvm +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 + +ENV LANGUAGE='en_US:en' + + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=185 build/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 build/quarkus-app/*.jar /deployments/ +COPY --chown=185 build/quarkus-app/app/ /deployments/app/ +COPY --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 +ENV AB_JOLOKIA_OFF="" +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + diff --git a/kotlin-quarkus/src/main/docker/Dockerfile.legacy-jar b/kotlin-quarkus/src/main/docker/Dockerfile.legacy-jar new file mode 100644 index 0000000..ab8ff1f --- /dev/null +++ b/kotlin-quarkus/src/main/docker/Dockerfile.legacy-jar @@ -0,0 +1,90 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=legacy-jar +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/kontor-legacy-jar . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-legacy-jar +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-legacy-jar +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 + +ENV LANGUAGE='en_US:en' + + +COPY build/lib/* /deployments/lib/ +COPY build/*-runner.jar /deployments/quarkus-run.jar + +EXPOSE 8080 +USER 185 +ENV AB_JOLOKIA_OFF="" +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" diff --git a/kotlin-quarkus/src/main/docker/Dockerfile.native b/kotlin-quarkus/src/main/docker/Dockerfile.native new file mode 100644 index 0000000..38e4141 --- /dev/null +++ b/kotlin-quarkus/src/main/docker/Dockerfile.native @@ -0,0 +1,27 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=native +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t quarkus/kontor . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor +# +### +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root build/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/kotlin-quarkus/src/main/docker/Dockerfile.native-micro b/kotlin-quarkus/src/main/docker/Dockerfile.native-micro new file mode 100644 index 0000000..cce62e8 --- /dev/null +++ b/kotlin-quarkus/src/main/docker/Dockerfile.native-micro @@ -0,0 +1,30 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# It uses a micro base image, tuned for Quarkus native executables. +# It reduces the size of the resulting container image. +# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=native +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/kontor . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor +# +### +FROM quay.io/quarkus/quarkus-micro-image:1.0 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root build/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/kotlin-quarkus/src/main/resources/META-INF/resources/index.html b/kotlin-quarkus/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000..f64aa10 --- /dev/null +++ b/kotlin-quarkus/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,289 @@ + + + + + kontor - 1.0.0-SNAPSHOT + + + +
+
+
+ + + + + quarkus_logo_horizontal_rgb_1280px_reverse + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+

You just made a Quarkus application.

+

This page is served by Quarkus.

+ Visit the Dev UI +

This page: src/main/resources/META-INF/resources/index.html

+

App configuration: src/main/resources/application.yml

+

Static assets: src/main/resources/META-INF/resources/

+

Code: src/main/java

+

Generated starter code:

+
    +
  • + RESTEasy Reactive Easily start your Reactive RESTful Web Services +
    @Path: /hello +
    Related guide +
  • + +
+
+
+

Selected extensions

+
    +
  • RESTEasy Reactive Jackson
  • +
  • JDBC Driver - H2 (guide)
  • +
  • Hibernate ORM with Panache and Kotlin (guide)
  • +
  • SmallRye OpenAPI (guide)
  • +
  • YAML Configuration (guide)
  • +
  • Hibernate Reactive Panache Kotlin
  • +
  • SmallRye Health (guide)
  • +
+
Documentation
+

Practical step-by-step guides to help you achieve a specific goal. Use them to help get your work + done.

+
Set up your IDE
+

Everyone has a favorite IDE they like to use to code. Learn how to configure yours to maximize your + Quarkus productivity.

+
+
+
+ + diff --git a/kotlin-quarkus/src/main/resources/application.yml b/kotlin-quarkus/src/main/resources/application.yml new file mode 100644 index 0000000..527a35f --- /dev/null +++ b/kotlin-quarkus/src/main/resources/application.yml @@ -0,0 +1,2 @@ +greeting: + message: "hello" diff --git a/kotlin-quarkus/src/native-test/java/de/thpeetz/kontor/GreetingResourceIT.java b/kotlin-quarkus/src/native-test/java/de/thpeetz/kontor/GreetingResourceIT.java new file mode 100644 index 0000000..ef71448 --- /dev/null +++ b/kotlin-quarkus/src/native-test/java/de/thpeetz/kontor/GreetingResourceIT.java @@ -0,0 +1,8 @@ +package de.thpeetz.kontor; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class GreetingResourceIT extends GreetingResourceTest { + // Execute the same tests but in packaged mode. +} diff --git a/kotlin-spring/.gitignore b/kotlin-spring/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/kotlin-spring/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/kotlin-spring/README.md b/kotlin-spring/README.md new file mode 100644 index 0000000..0473319 --- /dev/null +++ b/kotlin-spring/README.md @@ -0,0 +1,92 @@ +# Kontor Kotlin + + + +## Getting started + +To make it easy for you to get started with GitLab, here's a list of recommended next steps. + +Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! + +## Add your files + +- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files +- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: + +``` +cd existing_repo +git remote add origin https://gitlab.thpeetz.de/kontor/kontor-kotlin.git +git branch -M main +git push -uf origin main +``` + +## Integrate with your tools + +- [ ] [Set up project integrations](https://gitlab.thpeetz.de/kontor/kontor-kotlin/-/settings/integrations) + +## Collaborate with your team + +- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) +- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) +- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) +- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) +- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) + +## Test and Deploy + +Use the built-in continuous integration in GitLab. + +- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) +- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) +- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) +- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) +- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) + +*** + +# Editing this README + +When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. + +## Suggestions for a good README +Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. + +## Name +Choose a self-explaining name for your project. + +## Description +Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. + +## Badges +On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. + +## Visuals +Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. + +## Installation +Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. + +## Usage +Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. + +## Support +Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. + +## Roadmap +If you have ideas for releases in the future, it is a good idea to list them in the README. + +## Contributing +State if you are open to contributions and what your requirements are for accepting them. + +For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. + +You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. + +## Authors and acknowledgment +Show your appreciation to those who have contributed to the project. + +## License +For open source projects, say how it is licensed. + +## Project status +If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. diff --git a/kotlin-spring/build.gradle b/kotlin-spring/build.gradle new file mode 100644 index 0000000..c6df720 --- /dev/null +++ b/kotlin-spring/build.gradle @@ -0,0 +1,58 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id 'org.springframework.boot' version '2.7.7' + id 'io.spring.dependency-management' version '1.0.15.RELEASE' + id 'org.jetbrains.kotlin.jvm' version '1.7.21' + id 'org.jetbrains.kotlin.plugin.spring' version '1.7.21' + id 'org.jetbrains.kotlin.plugin.jpa' version '1.7.21' + id 'org.jetbrains.kotlin.plugin.allopen' version '1.7.21' + id 'org.jetbrains.kotlin.kapt' version '1.7.21' +} + +group = 'de.thpeetz.kontor' +version = '0.0.1-SNAPSHOT' +sourceCompatibility = '11' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-mustache' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' + implementation 'org.jetbrains.kotlin:kotlin-reflect' + implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + kapt 'org.springframework.boot:spring-boot-configuration-processor' + runtimeOnly 'com.h2database:h2' + runtimeOnly 'org.springframework.boot:spring-boot-devtools' + testImplementation("org.springframework.boot:spring-boot-starter-test") { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + exclude module: 'mockito-core' + } + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'com.ninja-squad:springmockk:3.1.1' + + +} + +tasks.withType(KotlinCompile) { + kotlinOptions { + freeCompilerArgs = ['-Xjsr305=strict'] + jvmTarget = '11' + } +} + +tasks.named('test') { + useJUnitPlatform() +} + +allOpen { + annotation("javax.persistence.Entity") + annotation("javax.persistence.Embeddable") + annotation("javax.persistence.MappedSuperclass") +} diff --git a/kotlin-spring/gradle/wrapper/gradle-wrapper.jar b/kotlin-spring/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/kotlin-spring/gradlew.bat b/kotlin-spring/gradlew.bat new file mode 100644 index 0000000..53a6b23 --- /dev/null +++ b/kotlin-spring/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kotlin-spring/settings.gradle b/kotlin-spring/settings.gradle new file mode 100644 index 0000000..185aa4e --- /dev/null +++ b/kotlin-spring/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'kontor' diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorApplication.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorApplication.kt new file mode 100644 index 0000000..279b7db --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorApplication.kt @@ -0,0 +1,16 @@ +package de.thpeetz.kontor + +import org.springframework.boot.Banner +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.runApplication + +@SpringBootApplication +@EnableConfigurationProperties(KontorProperties::class) +class KontorApplication + +fun main(args: Array) { + runApplication(*args) { + setBannerMode(Banner.Mode.OFF) + } +} diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorConfiguration.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorConfiguration.kt new file mode 100644 index 0000000..e54f2f9 --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorConfiguration.kt @@ -0,0 +1,79 @@ +package de.thpeetz.kontor + +import de.thpeetz.kontor.comics.* +import org.springframework.boot.ApplicationRunner +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class KontorConfiguration { + @Bean + fun databaseInitializer(artistRepository: ArtistRepository, + publisherRepository: PublisherRepository, + comicRepository: ComicRepository, + issueRepository: IssueRepository + ) = ApplicationRunner { + + artistRepository.save(Artist("Turner, Michael")) + artistRepository.save(Artist("Bendis, Brian Michael")) + artistRepository.save(Artist("Land, Greg")) + artistRepository.save(Artist("Whedon, Joss")) + val marvel = publisherRepository.save(Publisher("Marvel")) + val aspen = publisherRepository.save(Publisher("Aspen")) + publisherRepository.save(Publisher("DC")) + val de = publisherRepository.save(Publisher("Dynamite Entertainment")) + val wildstorm = publisherRepository.save(Publisher("WildStorm")) + val bongo = publisherRepository.save(Publisher("Bongo")) + val image = publisherRepository.save(Publisher("Image")) + val darkHorse = publisherRepository.save(Publisher("Dark Horse Comics")) + val cliffhanger = publisherRepository.save(Publisher("Cliffhanger")) + comicRepository.save(Comic(title = "X-Men", publisher = marvel, currentOrder = false, completed = false)) + val redSonja = comicRepository.save(Comic(title = "Red Sonja", publisher = de, currentOrder = false, completed = false)) + val x23 = comicRepository.save(Comic(title = "X-23", publisher = marvel, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Simpsons Comics", publisher = bongo, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Futurama Comics", publisher = bongo, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Bomb Queen III: The Good, The Bad and The Lovely", publisher = image, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Bomb Queen IV: Suicide Bomber", publisher = image, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Gen13", publisher = wildstorm, currentOrder = false, completed = false)) + val bombqueen = comicRepository.save(Comic(title = "Bomb Queen II: Queen of Hearts", publisher = image, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Iron & The Maiden",publisher = aspen,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Fathom",publisher = aspen,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Soulfire",publisher = aspen,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Star Wars: Rebellion",publisher = darkHorse,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Star Wars: Rebellion",publisher = darkHorse,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Star Wars: Knights of the Old Republic",publisher = darkHorse,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Star Wars: Legacy",publisher = darkHorse,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Star Wars: Dark Times", publisher = darkHorse, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Samurai: Heaven and Earth", publisher = darkHorse, currentOrder = false, completed = false)) + val battlpope = comicRepository.save(Comic(title = "Battle Pope", publisher = image,currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Danger Girl", publisher = cliffhanger, currentOrder = false, completed = false)) + val marville = comicRepository.save(Comic(title = "Marville", publisher = marvel, currentOrder = false, completed = false)) + issueRepository.save(Issue(number = "0", comic = redSonja, gelesen = true)) + issueRepository.save(Issue(number = "1", comic = redSonja, gelesen = true)) + issueRepository.save(Issue(number = "2", comic = redSonja, gelesen = true)) + issueRepository.save(Issue(number = "1", comic = x23, gelesen = false)) + issueRepository.save(Issue(number = "1", comic = bombqueen, gelesen = false)) + issueRepository.save(Issue(number = "2", comic = bombqueen, gelesen = false)) + issueRepository.save(Issue(number = "3", comic = bombqueen, gelesen = false)) + issueRepository.save(Issue(number = "4", comic = bombqueen, gelesen = false)) + issueRepository.save(Issue(number = "1", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "2", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "3", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "4", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "5", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "6", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "7", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "8", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "9", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "10", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "11", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "12", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "1", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "2", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "3", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "4", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "5", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "6", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "7", comic = marville, gelesen = false)) + } +} diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorController.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorController.kt new file mode 100644 index 0000000..3d639dd --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorController.kt @@ -0,0 +1,24 @@ +package de.thpeetz.kontor + +import de.thpeetz.kontor.KontorProperties +import de.thpeetz.kontor.Navigation +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.ui.set +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping + +@Controller +@RequestMapping("/") +class KontorController(val properties: KontorProperties, val navigation: Navigation) { + @GetMapping("/") + fun blog(model: Model): String { + model["title"] = properties.title + model["title"] = "Kontor" + model["banner"] = properties.banner + model["version"] = properties.version + model["navigation"] = navigation.links() + model["login"] = navigation.login() + return "kontor" + } +} \ No newline at end of file diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorProperties.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorProperties.kt new file mode 100644 index 0000000..92c77c7 --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorProperties.kt @@ -0,0 +1,10 @@ +package de.thpeetz.kontor + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.context.properties.ConstructorBinding + +@ConstructorBinding +@ConfigurationProperties("kontor") +data class KontorProperties(var title: String, var version: String = "unknown", val banner: Banner) { + data class Banner(val title: String? = null, val content: String) +} \ No newline at end of file diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/Navigation.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/Navigation.kt new file mode 100644 index 0000000..fb730af --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/Navigation.kt @@ -0,0 +1,24 @@ +package de.thpeetz.kontor + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class Navigation { + + @Bean + fun links(): Iterable = listOf( + Link("/comics", "Comics"), + Link("/library","Library"), + Link("/office", "HomeOffice"), + Link("/tradingcards", "Trading Cards"), + Link("/tysc","TradeYourSportsCards"), + Link("/user/login", ""), + Link("/admin/","Admin") + ) + + @Bean + fun login(): Iterable = listOf(Link("/user/login", "Login")) +} + +data class Link(val link: String, val title: String) diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/RequestLoggingFilter.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/RequestLoggingFilter.kt new file mode 100644 index 0000000..decb70f --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/RequestLoggingFilter.kt @@ -0,0 +1,25 @@ +package de.thpeetz.kontor + +import org.slf4j.LoggerFactory +import javax.servlet.Filter +import org.springframework.stereotype.Component +import javax.servlet.FilterChain +import javax.servlet.ServletRequest +import javax.servlet.ServletResponse + +@Component +class RequestLoggingFilter: Filter { + val loggerFactory = LoggerFactory.getLogger("Kontor Logger") + + override fun doFilter( + request: ServletRequest, + response: ServletResponse, + filterChain: FilterChain + ) { + val requestString = request.servletContext.contextPath.toString() + //val attributeNames = request.attributeNames + //attributeNames.toList().forEach { loggerFactory.info("Attribute: ${it.toString()}") } + loggerFactory.info("Logging request: $requestString") + filterChain.doFilter(request, response) + } +} \ No newline at end of file diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsApiController.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsApiController.kt new file mode 100644 index 0000000..d2e3b4e --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsApiController.kt @@ -0,0 +1,25 @@ +package de.thpeetz.kontor.comics + +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("api/") +class ComicsApiController(val artistRepository: ArtistRepository, + val publisherRepository: PublisherRepository, + val comicRepository: ComicRepository, + val issueRepository: IssueRepository) { + + @GetMapping("/artist") + fun artistList() = artistRepository.findAll() + + @GetMapping("/publisher") + fun publisherList() = publisherRepository.findAll() + + @GetMapping("/comics") + fun comicsList() = comicRepository.findAll() + + @GetMapping("/issues") + fun issuesList() = issueRepository.findAll() +} diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsController.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsController.kt new file mode 100644 index 0000000..f339b04 --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsController.kt @@ -0,0 +1,29 @@ +package de.thpeetz.kontor.comics + +import de.thpeetz.kontor.KontorProperties +import de.thpeetz.kontor.Navigation +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.ui.set +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping + +@Controller +@RequestMapping("/comics") +class ComicsController(private val properties: KontorProperties, + val navigation: Navigation, + val comicRepository: ComicRepository +) { + + @GetMapping("/") + fun blog(model: Model): String { + model["title"] = properties.title + model["title"] = "Comics" + model["banner"] = properties.banner + model["navigation"] = navigation.links() + model["comics"] = comicRepository.findAll().map { it } + return "comics" + } + + +} diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Entities.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Entities.kt new file mode 100644 index 0000000..3be00f5 --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Entities.kt @@ -0,0 +1,36 @@ +package de.thpeetz.kontor.comics + +import javax.persistence.Entity +import javax.persistence.GeneratedValue +import javax.persistence.Id +import javax.persistence.ManyToOne + + +@Entity +class Artist( + val name: String, + @Id @GeneratedValue val id: Long? = null +) + +@Entity +class Publisher( + val name: String, + @Id @GeneratedValue val id: Long? = null +) + +@Entity +class Comic( + val title: String, + @ManyToOne val publisher: Publisher, + val currentOrder: Boolean, + val completed: Boolean, + @Id @GeneratedValue val id: Long? = null +) + +@Entity +class Issue( + val number: String, + @ManyToOne val comic: Comic, + val gelesen: Boolean, + @Id @GeneratedValue val id: Long? = null +) \ No newline at end of file diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Repositories.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Repositories.kt new file mode 100644 index 0000000..fe44b2e --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Repositories.kt @@ -0,0 +1,22 @@ +package de.thpeetz.kontor.comics + +import org.springframework.data.repository.CrudRepository + +interface ArtistRepository: CrudRepository { + + override fun findAll(): Iterable +} + +interface PublisherRepository: CrudRepository { + override fun findAll(): Iterable +} + +interface ComicRepository: CrudRepository { + + override fun findAll(): Iterable +} + +interface IssueRepository: CrudRepository { + + override fun findAll(): Iterable +} \ No newline at end of file diff --git a/kotlin-spring/src/main/resources/application.properties b/kotlin-spring/src/main/resources/application.properties new file mode 100644 index 0000000..6d27d3a --- /dev/null +++ b/kotlin-spring/src/main/resources/application.properties @@ -0,0 +1,6 @@ +spring.jpa.properties.hibernate.globally_quoted_identifiers=true +spring.jpa.properties.hibernate.globally_quoted_identifiers_skip_column_definitions = true +kontor.title=Kontor +kontor.version=1.0.0-SNAPSHOT +kontor.banner.title=Warning +kontor.banner.content=The blog will be down tomorrow. diff --git a/kotlin-spring/src/main/resources/templates/comics.mustache b/kotlin-spring/src/main/resources/templates/comics.mustache new file mode 100644 index 0000000..336d9f0 --- /dev/null +++ b/kotlin-spring/src/main/resources/templates/comics.mustache @@ -0,0 +1,23 @@ +{{> header}} + +{{> menu}} +

+ +
+ + + + {{#comics}} + + {{/comics}} +
List of Comics
Name
{{title}}
+
+ Add entry +
+
+ +{{> footer}} diff --git a/kotlin-spring/src/main/resources/templates/footer.mustache b/kotlin-spring/src/main/resources/templates/footer.mustache new file mode 100644 index 0000000..dde9847 --- /dev/null +++ b/kotlin-spring/src/main/resources/templates/footer.mustache @@ -0,0 +1,22 @@ +
+ + + + diff --git a/kotlin-spring/src/main/resources/templates/header.mustache b/kotlin-spring/src/main/resources/templates/header.mustache new file mode 100644 index 0000000..d794c9e --- /dev/null +++ b/kotlin-spring/src/main/resources/templates/header.mustache @@ -0,0 +1,15 @@ + + + + + {{title}} + + + + + + + + + + diff --git a/kotlin-spring/src/main/resources/templates/kontor.mustache b/kotlin-spring/src/main/resources/templates/kontor.mustache new file mode 100644 index 0000000..177306f --- /dev/null +++ b/kotlin-spring/src/main/resources/templates/kontor.mustache @@ -0,0 +1,18 @@ +{{> header}} + +{{> menu}} + +
+ {{#banner.title}} +
+ + +
+ {{/banner.title}} +
+ +{{> footer}} diff --git a/kotlin-spring/src/main/resources/templates/menu.mustache b/kotlin-spring/src/main/resources/templates/menu.mustache new file mode 100644 index 0000000..c4d9ba0 --- /dev/null +++ b/kotlin-spring/src/main/resources/templates/menu.mustache @@ -0,0 +1,23 @@ + diff --git a/kotlin-spring/src/test/resources/junit-platform.properties b/kotlin-spring/src/test/resources/junit-platform.properties new file mode 100644 index 0000000..ab79697 --- /dev/null +++ b/kotlin-spring/src/test/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.testinstance.lifecycle.default = per_class diff --git a/wicket/.github/workflows/gradle.yml b/wicket/.github/workflows/gradle.yml new file mode 100644 index 0000000..58e1c59 --- /dev/null +++ b/wicket/.github/workflows/gradle.yml @@ -0,0 +1,26 @@ +# This workflow will build a Java project with Gradle +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Java CI with Gradle + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build diff --git a/wicket/.gitignore b/wicket/.gitignore new file mode 100644 index 0000000..2e1f2c3 --- /dev/null +++ b/wicket/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +bin/ +.classpath +.project +.settings/ +.asciidoctorconfig.adoc diff --git a/wicket/.gitlab-ci.yml b/wicket/.gitlab-ci.yml new file mode 100644 index 0000000..514cefd --- /dev/null +++ b/wicket/.gitlab-ci.yml @@ -0,0 +1,36 @@ +variables: + GRADLE_OPTS: "-Dorg.gradle.daemon=false" + +before_script: + - source "/home/gitlab-runner/.sdkman/bin/sdkman-init.sh" + - sdk u java 11.0.12-open + +stages: + - build + - test + - analysis + - publish + +Build Application: + stage: build + script: ./gradlew build + +Create Documentation: + stage: build + script: ./gradlew asciidoctorPdf + +Test Application: + stage: test + script: ./gradlew check + +sonarqube-check: + stage: analysis + variables: + SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar" # Defines the location of the analysis task cache + GIT_DEPTH: "0" # Tells git to fetch all the branches of the project, required by the analysis task + script: ./gradlew sonarqube + allow_failure: true + +Publish Artifacts: + stage: publish + script: ./gradlew publish diff --git a/wicket/README.md b/wicket/README.md new file mode 100644 index 0000000..ab12d07 --- /dev/null +++ b/wicket/README.md @@ -0,0 +1,4 @@ +![Java CI with Gradle](https://github.com/tpeetz/kontor-wicket/workflows/Java%20CI%20with%20Gradle/badge.svg) + +# kontor-wicket +Kontor Application with Apache Wicket diff --git a/wicket/build.gradle b/wicket/build.gradle new file mode 100644 index 0000000..0afbe9f --- /dev/null +++ b/wicket/build.gradle @@ -0,0 +1,61 @@ +plugins { + id 'war' + id 'jacoco-report-aggregation' + alias(versions.plugins.asciidoctorConvention) + alias(versions.plugins.javaConvention) + alias(versions.plugins.sonarqube) +} + +final BUILD_DATE = new Date().format('dd.MM.yyyy').toString() + +dependencies { + implementation versions.slf4j + implementation versions.commonscli + testImplementation versions.junit + implementation versions.bundles.logback + spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.12.0' + + implementation 'org.apache.wicket:wicket:9.9.1' + implementation 'org.apache.wicket:wicket-extensions:9.9.1' + testImplementation 'org.eclipse.jetty:jetty-webapp:9.4.44.v20210927' + testImplementation 'org.eclipse.jetty:jetty-jmx:9.4.44.v20210927' +} + +publishing { + publications { + application(MavenPublication) { + groupId = group + from components.java + } + } +} + +jacocoTestReport { + reports { + xml.enabled true + } +} + +test.finalizedBy jacocoTestReport + +spotbugs { + ignoreFailures = true +} + +sonarqube { + properties { + property "sonar.projectKey", "kontor_kontor-wicket_AYCAQ47WRCd9N673sSSD" + property "sonar.host.url", "https://sonar.thpeetz.de" + property "sonar.login", "7cf604ab1ebf48f7dc60d942c8196a132b228a6d" + property "sonar.qualitygate.wait", true + property "sonar.sourceEncoding", "UTF-8" + } +} + +tasks.named('sonarqube').configure { + dependsOn test +} + +wrapper { + gradleVersion = "7.5" +} diff --git a/wicket/gradle.properties b/wicket/gradle.properties new file mode 100644 index 0000000..e140eca --- /dev/null +++ b/wicket/gradle.properties @@ -0,0 +1,3 @@ +description='Anwendung Kontor mit Apache Wicket' +group=de.thpeetz +version=1.0.0-SNAPSHOT diff --git a/wicket/gradle/wrapper/gradle-wrapper.jar b/wicket/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/wicket/gradlew.bat b/wicket/gradlew.bat new file mode 100644 index 0000000..53a6b23 --- /dev/null +++ b/wicket/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/wicket/settings.gradle b/wicket/settings.gradle new file mode 100644 index 0000000..f66ecb4 --- /dev/null +++ b/wicket/settings.gradle @@ -0,0 +1 @@ +rootProject.name = "kontor-wicket" diff --git a/wicket/src/docs/asciidoc/kontor-wicket.adoc b/wicket/src/docs/asciidoc/kontor-wicket.adoc new file mode 100644 index 0000000..e310b6f --- /dev/null +++ b/wicket/src/docs/asciidoc/kontor-wicket.adoc @@ -0,0 +1,179 @@ += Projektbeschreibung kontor-wicket: Entwicklungs- und Projekthandbuch +:author: Thomas Peetz +:email: +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: +:toclevels: 4 +:table-caption!: +:counter: table-number: 0 + +[title="Dokumenthistorie", id="Table-{counter:table-number}", options="header"] +|=== +| Version | Datum | Autor | Änderungsgrund / Bemerkungen +| 1.0.0 | 16.05.2022 | Thomas Peetz | Ersterstellung +|=== + +== Allgemeines + +=== Zweck des Dokumentes + +Das Entwicklungshandbuch beschreibt die Werkzeuge und die Vorgehensweise bei der Entwicklung +im Projekt kontor-wicket und der Erstellung der Dokumentation. + +=== Verwendete Tools + +==== Gitlab + +Für die Verwaltung des Sourcecode kommt ((Gitlab))<> zum Einsatz. +Mit Gitlab werden auch die Projektaufgaben verwaltet. + +Das Projekt und das dazugehörige Git Repository sind unter der Adresse + +https://gitlab.ingenieurbuero-peetz.de/kontor/kontor-wicket + +zu finden. + +== Erstellung der Dokumentation + +Die Dokumentation des Projektes wird mit ((Asciidoctor))<> geschrieben. +Die Dokumente erhalten ihre Namen nach dem jeweiligen Hauptdokument. + +=== Quellcode Verwaltung + +Die Asciidoctor-Dateien haben die Endung `.adoc`. + +=== Buildsystem + +Zur Erstellung der PDF-Dateien aus den Asciidoctor-Dateien wird das Buildsystem ((Gradle))<<3>> verwendet. +Die Dateien für die Dokumente liegen im Verzeichnis `src/docs/asciidoc`. + +Der Gradle Build wird über die Datei `build.gradle` definiert. + + +== Einführung + +=== Zweck + +=== Stakeholder des Systems + +=== Systemumfang + +==== Zielsetzung des Systems + +=== Systemübersicht + +==== Systemkontext + +==== Systemarchitektur + +==== Systemschnittstellen + +===== Realisierte Schnittstellen + +===== Verwendete Schnittstellen + +==== Logisches Datenmodell + +==== Einschränkungen + +== Anforderungen der Domäne + +=== Systemfunktionen + +==== Anwendungsfälle + +==== Akteure + +==== Zielgruppen + +=== Anforderungen + +==== Anforderungen an externe Schnittstellen + +==== Funktionale Anforderungen + +==== Qualitätsanforderungen + +==== Randbedingungen + +==== Weitere Anforderungen + +==== Wartungs- und Supportinformationen + +=== Verifikation + +== Projektbeschreibung + +=== Ausgangslage + +//==== Rechtliche Vorgaben und Rahmenbedingungen +//=== Rahmenbedingungen + +//==== Vorhandene Regelungen + +=== Projektziele + +=== Projektabgrenzung + +//=== Voraussichtliche Kosten + +//=== Projektrisiken + +//==== Produktivität + +//==== Finanzielle Risiken + +//==== Akzeptanz + +== Projektorganisation + +=== Projekt-Aufbauorganisation + +=== Rollendefinition + +//==== Projektauftraggeber + +//==== Projektausschuss + +//==== Beratung / Qualitätssicherung + +==== Projekteiter + +==== Projektteam + +==== Liste der Stakeholder + +=== Projektablauforganisation + +==== Projekt-Phasen + +===== Erstellung der Projektdokumentation + + +== Verschiedenes + +=== Erreichbarkeiten + +[bibliography] +== Referenzen + +- [[[asciidoctor]]] http://asciidoctor.org +- [[[gitlab]]] http://www.gitlab.org +- [[[gradle]]] http://www.gradle.org +- [[[jenkins]]] http://jenkins-ci.org + +[glossary] +== Glossar + +[index] +== Index + +== Verzeichnisse + +=== Abbildungsverzeichnis + +=== Tabellenverzeichnis + +<> <> diff --git a/wicket/src/main/java/de/thpeetz/kontor/HomePage.java b/wicket/src/main/java/de/thpeetz/kontor/HomePage.java new file mode 100644 index 0000000..e5a3c82 --- /dev/null +++ b/wicket/src/main/java/de/thpeetz/kontor/HomePage.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor; + +import org.apache.wicket.request.mapper.parameter.PageParameters; +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.markup.html.WebPage; + +public class HomePage extends WebPage { + private static final long serialVersionUID = 1L; + + public HomePage(final PageParameters parameters) { + super(parameters); + + add(new Label("version", getApplication().getFrameworkSettings().getVersion())); + + // TODO Add your page's components here + + } +} diff --git a/wicket/src/main/java/de/thpeetz/kontor/KontorApplication.java b/wicket/src/main/java/de/thpeetz/kontor/KontorApplication.java new file mode 100644 index 0000000..9c39858 --- /dev/null +++ b/wicket/src/main/java/de/thpeetz/kontor/KontorApplication.java @@ -0,0 +1,41 @@ +package de.thpeetz.kontor; + +import org.apache.wicket.csp.CSPDirective; +import org.apache.wicket.csp.CSPDirectiveSrcValue; +import org.apache.wicket.markup.html.WebPage; +import org.apache.wicket.protocol.http.WebApplication; + +/** + * Application object for your web application. + * If you want to run this application without deploying, run the Start class. + * + * @see de.thpeetz.kontor.Start#main(String[]) + */ +public class KontorApplication extends WebApplication +{ + /** + * @see org.apache.wicket.Application#getHomePage() + */ + @Override + public Class getHomePage() + { + return HomePage.class; + } + + /** + * @see org.apache.wicket.Application#init() + */ + @Override + public void init() + { + super.init(); + + // needed for the styling used by the quickstart + getCspSettings().blocking() + .add(CSPDirective.STYLE_SRC, CSPDirectiveSrcValue.SELF) + .add(CSPDirective.STYLE_SRC, "https://fonts.googleapis.com/css") + .add(CSPDirective.FONT_SRC, "https://fonts.gstatic.com"); + + // add your configuration here + } +} diff --git a/wicket/src/main/resources/de/thpeetz/kontor/HomePage.html b/wicket/src/main/resources/de/thpeetz/kontor/HomePage.html new file mode 100644 index 0000000..4ca32fc --- /dev/null +++ b/wicket/src/main/resources/de/thpeetz/kontor/HomePage.html @@ -0,0 +1,62 @@ + + + + + Apache Wicket Quickstart + + + + +

+ +
+
+

Congratulations!

+

+ Your quick start works! This project is especially useful to + start developing your Wicket application or to create a test + case for a bug report. +

+

Get started

+

+ You can even switch to HTTPS! +

+

+ From here you can start hacking away at your application and + wow your clients: +

+ +

Get help

+

+ We are here to help! +

+ +

Reporting a bug

+

+ Help us help you: +

+
    +
  1. reproduce the bug with the least amount of code
  2. +
  3. create a unit test that shows the bug
  4. +
  5. fix the bug and create a patch
  6. +
  7. attach the result of step 1, 2 or 3 to a JIRA issue
  8. +
  9. profit!
  10. +
+

+ Please mention the correct Wicket version: 1.5-SNAPSHOT. +

+
+
+
+ + diff --git a/wicket/src/main/webapp/WEB-INF/web.xml b/wicket/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..f5b288e --- /dev/null +++ b/wicket/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,32 @@ + + + + kontor-wicket + + + + + wicket.kontor-wicket + org.apache.wicket.protocol.http.WicketFilter + + applicationClassName + de.thpeetz.kontor.KontorApplication + + + + + wicket.kontor-wicket + /* + + diff --git a/wicket/src/main/webapp/logo.png b/wicket/src/main/webapp/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..39ec54854b748ab6aeb6b3965d88f452772d7d8e GIT binary patch literal 12244 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 i1O^!^0gjqN1OFf9qq%!F8)!QK0000`K literal 0 HcmV?d00001 diff --git a/wicket/src/main/webapp/style.css b/wicket/src/main/webapp/style.css new file mode 100644 index 0000000..87576a7 --- /dev/null +++ b/wicket/src/main/webapp/style.css @@ -0,0 +1,68 @@ +body, p, li, a { font-family: georgia, times, serif;font-size:13pt;} +h1, h2, h3 { font-family: 'Yanone Kaffeesatz', arial, serif; } +body { margin:0;padding:0;} +#hd { + width : 100%; + height : 87px; + background-color : #092E67; + margin-top : 0; + padding-top : 10px; + border-bottom : 1px solid #888; + z-index : 0; +} +#ft { + position : absolute; + bottom : 0; + width : 100%; + height : 99px; + background-color : #6493D2; + border-top : 1px solid #888; + z-index : 0; +} +#logo,#bd { + width : 650px; + margin: 0 auto; + padding: 25px 50px 0 50px; +} +#logo h1 { + color : white; + font-size:36pt; + display: inline; +} +#logo img { + display:inline; + vertical-align: bottom; + margin-left : 50px; + margin-right : 5px; +} +body { margin-top : 0; padding-top : 0;} +#logo, #logo h1 { margin-top : 0; padding-top : 0;} +#bd { + position : absolute; + top : 75px; + bottom : 75px; + left : 50%; + margin-left : -325px; + z-index : 1; + overflow: auto; + background-color : #fff; + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; + -moz-box-shadow: 0px 0px 10px #888; + -webkit-box-shadow: 0px 0px 10px #888; + box-shadow: 0px 0px 10px #888; +} +a, a:visited, a:hover, a:active { + color : #6493D2; +} +h2 { + padding : 0; margin:0; + font-size:36pt; + color:#FF5500; +} +h3 { + padding : 0; margin:0; + font-size:24pt; + color:#092E67; +} diff --git a/wicket/src/test/java/de/thpeetz/kontor/Start.java b/wicket/src/test/java/de/thpeetz/kontor/Start.java new file mode 100644 index 0000000..49c740a --- /dev/null +++ b/wicket/src/test/java/de/thpeetz/kontor/Start.java @@ -0,0 +1,106 @@ +package de.thpeetz.kontor; + +import java.lang.management.ManagementFactory; + +import javax.management.MBeanServer; + +import org.eclipse.jetty.jmx.MBeanContainer; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.webapp.WebAppContext; + +/** + * Separate startup class for people that want to run the examples directly. Use parameter + * -Dcom.sun.management.jmxremote to startup JMX (and e.g. connect with jconsole). + */ +public class Start +{ + /** + * Main function, starts the jetty server. + * + * @param args + */ + public static void main(String[] args) + { + System.setProperty("wicket.configuration", "development"); + + Server server = new Server(); + + HttpConfiguration http_config = new HttpConfiguration(); + http_config.setSecureScheme("https"); + http_config.setSecurePort(8443); + http_config.setOutputBufferSize(32768); + + ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(http_config)); + http.setPort(8080); + http.setIdleTimeout(1000 * 60 * 60); + + server.addConnector(http); + + Resource keystore = Resource.newClassPathResource("/keystore.p12"); + if (keystore != null && keystore.exists()) + { + // if a keystore for a SSL certificate is available, start a SSL + // connector on port 8443. + // By default, the quickstart comes with a Apache Wicket Quickstart + // Certificate that expires about half way september 2031. Do not + // use this certificate anywhere important as the passwords are + // available in the source. + + SslContextFactory sslContextFactory = new SslContextFactory(); + sslContextFactory.setKeyStoreResource(keystore); + sslContextFactory.setKeyStorePassword("wicket"); + sslContextFactory.setKeyManagerPassword("wicket"); + + HttpConfiguration https_config = new HttpConfiguration(http_config); + https_config.addCustomizer(new SecureRequestCustomizer()); + + ServerConnector https = new ServerConnector(server, new SslConnectionFactory( + sslContextFactory, "http/1.1"), new HttpConnectionFactory(https_config)); + https.setPort(8443); + https.setIdleTimeout(500000); + + server.addConnector(https); + System.out.println("SSL access to the examples has been enabled on port 8443"); + System.out + .println("You can access the application using SSL on https://localhost:8443"); + System.out.println(); + } + + WebAppContext bb = new WebAppContext(); + bb.setServer(server); + bb.setContextPath("/"); + bb.setWar("src/main/webapp"); + + // uncomment the next two lines if you want to start Jetty with WebSocket (JSR-356) support + // you need org.apache.wicket:wicket-native-websocket-javax in the classpath! + // ServerContainer serverContainer = WebSocketServerContainerInitializer.configureContext(bb); + // serverContainer.addEndpoint(new WicketServerEndpointConfig()); + + // uncomment next line if you want to test with JSESSIONID encoded in the urls + // ((AbstractSessionManager) + // bb.getSessionHandler().getSessionManager()).setUsingCookies(false); + + server.setHandler(bb); + + MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); + MBeanContainer mBeanContainer = new MBeanContainer(mBeanServer); + server.addEventListener(mBeanContainer); + server.addBean(mBeanContainer); + + try + { + server.start(); + server.join(); + } catch (Exception e) { + e.printStackTrace(); + System.exit(100); + } + } +} diff --git a/wicket/src/test/java/de/thpeetz/kontor/TestHomePage.java b/wicket/src/test/java/de/thpeetz/kontor/TestHomePage.java new file mode 100644 index 0000000..fe9c409 --- /dev/null +++ b/wicket/src/test/java/de/thpeetz/kontor/TestHomePage.java @@ -0,0 +1,29 @@ +package de.thpeetz.kontor; + +import org.apache.wicket.util.tester.WicketTester; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Simple test using the WicketTester + */ +public class TestHomePage +{ + private WicketTester tester; + + @BeforeEach + public void setUp() + { + tester = new WicketTester(new KontorApplication()); + } + + @Test + public void homepageRendersSuccessfully() + { + //start and render the test page + tester.startPage(HomePage.class); + + //assert rendered page class + tester.assertRenderedPage(HomePage.class); + } +} diff --git a/wicket/src/test/resources/jetty/jetty-http.xml b/wicket/src/test/resources/jetty/jetty-http.xml new file mode 100644 index 0000000..7b39acb --- /dev/null +++ b/wicket/src/test/resources/jetty/jetty-http.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wicket/src/test/resources/jetty/jetty-https.xml b/wicket/src/test/resources/jetty/jetty-https.xml new file mode 100644 index 0000000..35100e7 --- /dev/null +++ b/wicket/src/test/resources/jetty/jetty-https.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + http/1.1 + + + + + + + + + + + + + 30000 + + + + diff --git a/wicket/src/test/resources/jetty/jetty-ssl.xml b/wicket/src/test/resources/jetty/jetty-ssl.xml new file mode 100644 index 0000000..f23231b --- /dev/null +++ b/wicket/src/test/resources/jetty/jetty-ssl.xml @@ -0,0 +1,57 @@ + + + + + + + + + / + + + + + + SSL_RSA_WITH_DES_CBC_SHA + SSL_DHE_RSA_WITH_DES_CBC_SHA + SSL_DHE_DSS_WITH_DES_CBC_SHA + SSL_RSA_EXPORT_WITH_RC4_40_MD5 + SSL_RSA_EXPORT_WITH_DES40_CBC_SHA + SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA + SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA + TLS_RSA_WITH_AES_256_GCM_SHA384 + TLS_RSA_WITH_AES_128_GCM_SHA256 + TLS_RSA_WITH_AES_256_CBC_SHA256 + TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA + TLS_RSA_WITH_AES_256_CBC_SHA + TLS_RSA_WITH_AES_256_CBC_SHA + TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA + TLS_ECDH_RSA_WITH_AES_256_CBC_SHA + TLS_DHE_RSA_WITH_AES_256_CBC_SHA + TLS_DHE_DSS_WITH_AES_256_CBC_SHA + TLS_RSA_WITH_AES_128_CBC_SHA256 + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA + TLS_RSA_WITH_AES_128_CBC_SHA + TLS_RSA_WITH_AES_128_CBC_SHA + TLS_RSA_WITH_AES_128_CBC_SHA + TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA + TLS_ECDH_RSA_WITH_AES_128_CBC_SHA + TLS_DHE_RSA_WITH_AES_128_CBC_SHA + TLS_DHE_DSS_WITH_AES_128_CBC_SHA + + + + + + + + + + + + + + + diff --git a/wicket/src/test/resources/jetty/jetty.xml b/wicket/src/test/resources/jetty/jetty.xml new file mode 100644 index 0000000..5590715 --- /dev/null +++ b/wicket/src/test/resources/jetty/jetty.xml @@ -0,0 +1,23 @@ + + + + + + + + https + + + + 32768 + 8192 + 8192 + true + false + 512 + + + + + + diff --git a/wicket/src/test/resources/keystore b/wicket/src/test/resources/keystore new file mode 100644 index 0000000000000000000000000000000000000000..1473db33287b4de9be58dc79741ac9d5c088919a GIT binary patch literal 3954 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 e{K-i+ON5SRjUU_1uu~kDP_^p8r(#`g+`j Date: Thu, 9 Jan 2025 22:28:46 +0100 Subject: [PATCH 19/91] add repo kontor-quarkus --- java-quarkus/.gitignore | 9 + java-quarkus/.gitlab-ci.yml | 38 +++ java-quarkus/README.md | 60 ++++ java-quarkus/build.gradle | 158 ++++++++++ java-quarkus/config/checkstyle/checkstyle.xml | 14 + java-quarkus/gradle.properties | 9 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 58910 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + java-quarkus/gradlew | 185 ++++++++++++ java-quarkus/gradlew.bat | 104 +++++++ java-quarkus/settings.gradle | 11 + .../src/docs/asciidoc/kontor-quarkus.adoc | 84 ++++++ java-quarkus/src/main/docker/Dockerfile.jvm | 94 ++++++ .../src/main/docker/Dockerfile.legacy-jar | 90 ++++++ .../src/main/docker/Dockerfile.native | 27 ++ .../src/main/docker/Dockerfile.native-micro | 30 ++ .../thpeetz/kontor/comics/ComicsResource.java | 95 ++++++ .../kontor/comics/service/ArtistService.java | 28 ++ .../kontor/comics/service/ComicsService.java | 12 + .../comics/service/PublisherService.java | 28 ++ .../resources/META-INF/resources/index.html | 284 ++++++++++++++++++ .../src/main/resources/application.properties | 3 + .../kontor/comics/ComicsResourceIT.java | 8 + .../kontor/comics/ComicsResourceTest.java | 19 ++ 24 files changed, 1395 insertions(+) create mode 100644 java-quarkus/.gitignore create mode 100644 java-quarkus/.gitlab-ci.yml create mode 100644 java-quarkus/README.md create mode 100644 java-quarkus/build.gradle create mode 100644 java-quarkus/config/checkstyle/checkstyle.xml create mode 100644 java-quarkus/gradle.properties create mode 100644 java-quarkus/gradle/wrapper/gradle-wrapper.jar create mode 100644 java-quarkus/gradle/wrapper/gradle-wrapper.properties create mode 100644 java-quarkus/gradlew create mode 100644 java-quarkus/gradlew.bat create mode 100644 java-quarkus/settings.gradle create mode 100644 java-quarkus/src/docs/asciidoc/kontor-quarkus.adoc create mode 100644 java-quarkus/src/main/docker/Dockerfile.jvm create mode 100644 java-quarkus/src/main/docker/Dockerfile.legacy-jar create mode 100644 java-quarkus/src/main/docker/Dockerfile.native create mode 100644 java-quarkus/src/main/docker/Dockerfile.native-micro create mode 100644 java-quarkus/src/main/java/de/thpeetz/kontor/comics/ComicsResource.java create mode 100644 java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ArtistService.java create mode 100644 java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ComicsService.java create mode 100644 java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/PublisherService.java create mode 100644 java-quarkus/src/main/resources/META-INF/resources/index.html create mode 100644 java-quarkus/src/main/resources/application.properties create mode 100644 java-quarkus/src/native-test/java/de/thpeetz/kontor/comics/ComicsResourceIT.java create mode 100644 java-quarkus/src/test/java/de/thpeetz/kontor/comics/ComicsResourceTest.java diff --git a/java-quarkus/.gitignore b/java-quarkus/.gitignore new file mode 100644 index 0000000..cec4d77 --- /dev/null +++ b/java-quarkus/.gitignore @@ -0,0 +1,9 @@ +build/ +.settings/ +.gradle/ +.classpath +.project +.vscode/ +bin/ +.idea/ +/.eclipse-pmd diff --git a/java-quarkus/.gitlab-ci.yml b/java-quarkus/.gitlab-ci.yml new file mode 100644 index 0000000..ed4caf6 --- /dev/null +++ b/java-quarkus/.gitlab-ci.yml @@ -0,0 +1,38 @@ +variables: + GRADLE_OPTS: "-Dorg.gradle.daemon=false" + +before_script: + - source "/home/gitlab-runner/.sdkman/bin/sdkman-init.sh" + - sdk u java 11.0.12-open + - chmod +x ./gradlew + +stages: + - build + - test + - analysis + - publish + +Build Application: + stage: build + script: ./gradlew --build-cache compileJava + +Create Documentation: + stage: build + script: ./gradlew asciidoctorPdf + +Test Application: + stage: test + script: ./gradlew check + +sonarqube-check: + stage: analysis + variables: + SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar" # Defines the location of the analysis task cache + GIT_DEPTH: "0" # Tells git to fetch all the branches of the project, required by the analysis task + script: ./gradlew sonarqube + allow_failure: true + +Publish Artifacts: + stage: publish + script: ./gradlew publish + diff --git a/java-quarkus/README.md b/java-quarkus/README.md new file mode 100644 index 0000000..11ec472 --- /dev/null +++ b/java-quarkus/README.md @@ -0,0 +1,60 @@ +# kontor-quarkus Project + +This project uses Quarkus, the Supersonic Subatomic Java Framework. + +If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ . + +## Running the application in dev mode + +You can run your application in dev mode that enables live coding using: +```shell script +./gradlew quarkusDev +``` + +> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. + +## Packaging and running the application + +The application can be packaged using: +```shell script +./gradlew build +``` +It produces the `quarkus-run.jar` file in the `build/quarkus-app/` directory. +Be aware that it’s not an _über-jar_ as the dependencies are copied into the `build/quarkus-app/lib/` directory. + +The application is now runnable using `java -jar build/quarkus-app/quarkus-run.jar`. + +If you want to build an _über-jar_, execute the following command: +```shell script +./gradlew build -Dquarkus.package.type=uber-jar +``` + +The application, packaged as an _über-jar_, is now runnable using `java -jar build/*-runner.jar`. + +## Creating a native executable + +You can create a native executable using: +```shell script +./gradlew build -Dquarkus.package.type=native +``` + +Or, if you don't have GraalVM installed, you can run the native executable build in a container using: +```shell script +./gradlew build -Dquarkus.package.type=native -Dquarkus.native.container-build=true +``` + +You can then execute your native executable with: `./build/kontor-quarkus-1.0.0-SNAPSHOT-runner` + +If you want to learn more about building native executables, please consult https://quarkus.io/guides/gradle-tooling. + +## Related Guides + +- RESTEasy Reactive ([guide](https://quarkus.io/guides/resteasy-reactive)): A JAX-RS implementation utilizing build time processing and Vert.x. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it. + +## Provided Code + +### RESTEasy Reactive + +Easily start your Reactive RESTful Web Services + +[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources) diff --git a/java-quarkus/build.gradle b/java-quarkus/build.gradle new file mode 100644 index 0000000..57e8102 --- /dev/null +++ b/java-quarkus/build.gradle @@ -0,0 +1,158 @@ +plugins { + id 'java' + id 'maven-publish' + id 'jacoco' + id 'jacoco-report-aggregation' + id 'pmd' + id 'checkstyle' + id 'jvm-test-suite' + id 'io.quarkus' + id "org.sonarqube" version "3.3" + alias(versionsLibs.plugins.asciidoctorConvert) + alias(versionsLibs.plugins.asciidoctorPdf) + alias(versionsLibs.plugins.asciidoctorGems) +} + +repositories { + ruby.gems() + maven { + url "https://nexus.thpeetz.de/nexus/content/groups/public/" + } + mavenCentral() + mavenLocal() +} + +dependencies { + implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") + implementation 'io.quarkus:quarkus-resteasy-reactive' + implementation 'io.quarkus:quarkus-resteasy-reactive-jackson' + implementation 'io.quarkus:quarkus-arc' + testImplementation 'io.quarkus:quarkus-junit5' + testImplementation 'io.rest-assured:rest-assured' + testImplementation("io.quarkus:quarkus-jacoco") +} + +final BUILD_DATE = new Date().format('dd.MM.yyyy').toString() +def pdfFile = layout.buildDirectory.file("docs/asciidocPdf/" + project.name + ".pdf") +def pdfArtifact = artifacts.add('archives', pdfFile.get().asFile) { + type 'pdf' + builtBy asciidoctorPdf +} + +tasks.withType(GenerateModuleMetadata).configureEach { + suppressedValidationErrors.add('enforced-platform') +} + +publishing { + publications { + quarkusApp(MavenPublication){ + from components.java + } + docs(MavenPublication) { + groupId = group + '.docs' + artifactId = project.name + artifact pdfArtifact + } + } + repositories { + maven { + name = 'nexusPeetz' + def releasesRepoUrl = "https://nexus.thpeetz.de/nexus/content/repositories/releases/" + def snapshotsRepoUrl = "https://nexus.thpeetz.de/nexus/content/repositories/snapshots/" + url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl + credentials(PasswordCredentials) + } + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +tasks.withType(GenerateModuleMetadata).configureEach { + suppressedValidationErrors.add('enforced-platform') +} + +compileJava { + options.encoding = 'UTF-8' + options.compilerArgs << '-parameters' +} + +compileTestJava { + options.encoding = 'UTF-8' +} + +jacocoTestReport { + reports { + xml.required = true + } +} + +test.finalizedBy jacocoTestReport + + +pmd { + //ruleSetFiles = files("custom-pmd-ruleset.xml") + ruleSets = ["category/java/errorprone.xml", "category/java/bestpractices.xml"] + ignoreFailures = true +} + +tasks.withType(Checkstyle) { + reports { + xml.required = true + html.required = true + } +} + +testing { + suites { + test { + useJUnitJupiter() + } + } +} + +pmdTest.dependsOn("compileQuarkusTestGeneratedSourcesJava") +checkstyleTest.dependsOn("compileQuarkusTestGeneratedSourcesJava") +jacocoTestReport.dependsOn("compileQuarkusGeneratedSourcesJava") +pmdMain.dependsOn("compileQuarkusGeneratedSourcesJava") + +sonarqube { + properties { + property "sonar.host.url", "https://sonar.thpeetz.de" + property "sonar.qualitygate.wait", true + property "sonar.sourceEncoding", "UTF-8" + property "sonar.projectKey", "kontor_kontor-quarkus_AYQek8Mxz0hBjLSV8I8O" + property "sonar.login", "5ecd90dee57806857e07443a9b0efd3cd7774a81" + } +} + +tasks.named('sonarqube').configure { + dependsOn 'test', 'pmdMain', 'checkstyleMain', 'jacocoTestReport' +} + +asciidoctorPdf { + baseDirFollowsSourceFile() + + asciidoctorj { + attributes 'build-gradle': file('build.gradle'), + 'endpoint-url': 'https://www.thpeetz.de', + 'source-highlighter': 'rouge', + 'imagesdir': './images', + 'toc': 'left', + 'toc-title': 'Inhaltsverzeichnis', + 'revdate': BUILD_DATE, + 'revnumber': { project.version.toString() }, + 'revremark': 'Entwurf', + 'chapter-label': '', + 'icons': 'font', + 'idprefix': 'id_', + 'idseparator': '-', + 'docinfo1': '' + } +} + +wrapper { + gradleVersion = "7.5" +} diff --git a/java-quarkus/config/checkstyle/checkstyle.xml b/java-quarkus/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..2574b06 --- /dev/null +++ b/java-quarkus/config/checkstyle/checkstyle.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java-quarkus/gradle.properties b/java-quarkus/gradle.properties new file mode 100644 index 0000000..c30b39f --- /dev/null +++ b/java-quarkus/gradle.properties @@ -0,0 +1,9 @@ +#Gradle properties +group=de.thpeetz.kontor +version=1.0.0-SNAPSHOT + +quarkusPluginId=io.quarkus +quarkusPluginVersion=2.13.2.Final +quarkusPlatformGroupId=io.quarkus.platform +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformVersion=2.13.2.Final \ No newline at end of file diff --git a/java-quarkus/gradle/wrapper/gradle-wrapper.jar b/java-quarkus/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..62d4c053550b91381bbd28b1afc82d634bf73a8a GIT binary patch literal 58910 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 literal 0 HcmV?d00001 diff --git a/java-quarkus/gradle/wrapper/gradle-wrapper.properties b/java-quarkus/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ae04661 --- /dev/null +++ b/java-quarkus/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/java-quarkus/gradlew b/java-quarkus/gradlew new file mode 100644 index 0000000..fbd7c51 --- /dev/null +++ b/java-quarkus/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/java-quarkus/gradlew.bat b/java-quarkus/gradlew.bat new file mode 100644 index 0000000..a9f778a --- /dev/null +++ b/java-quarkus/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/java-quarkus/settings.gradle b/java-quarkus/settings.gradle new file mode 100644 index 0000000..0ab2682 --- /dev/null +++ b/java-quarkus/settings.gradle @@ -0,0 +1,11 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + mavenLocal() + } + plugins { + id "${quarkusPluginId}" version "${quarkusPluginVersion}" + } +} +rootProject.name='kontor-quarkus' diff --git a/java-quarkus/src/docs/asciidoc/kontor-quarkus.adoc b/java-quarkus/src/docs/asciidoc/kontor-quarkus.adoc new file mode 100644 index 0000000..d5d7ac4 --- /dev/null +++ b/java-quarkus/src/docs/asciidoc/kontor-quarkus.adoc @@ -0,0 +1,84 @@ += Projektbeschreibung kontor-quarkus: Pflichtenheft und Projekthandbuch +:author: Thomas Peetz +:email: +:doctype: article +:sectnums: +:sectnumlevels: 4 +:toc: +:toclevels: 4 +:table-caption!: +:counter: table-number: 0 + +//[title="Dokumenthistorie", caption="Tabelle {counter:table-number} ", id="Tabelle-{counter:table-number}", options="header"] +//[title="Dokumenthistorie", id="Table-{counter:table-number}", options="header", cols="4"] +[title="Dokumenthistorie", id="Table-{counter:table-number}", options="header"] +|=== +| Version | Datum | Autor | Änderungsgrund / Bemerkungen +| 0.0.1 | 16.05.2022 | Thomas Peetz | Ersterstellung +|=== + +== Einführung + +=== Zweck + +=== Stakeholder des Systems + +=== Systemumfang + +==== Zielsetzung des Systems + +=== Systemübersicht + +==== Systemkontext + +==== Systemarchitektur + +==== Systemschnittstellen + +===== Realisierte Schnittstellen + +===== Verwendete Schnittstellen + +==== Logisches Datenmodell + +==== Einschränkungen + +== Anforderungen der Domäne + +=== Systemfunktionen + +==== Anwendungsfälle + +==== Akteure + +==== Zielgruppen + +=== Anforderungen + +==== Anforderungen an externe Schnittstellen + +==== Funktionale Anforderungen + +==== Qualitätsanforderungen + +==== Randbedingungen + +==== Weitere Anforderungen + +==== Wartungs- und Supportinformationen + +=== Verifikation + +[bibliography] +== Referenzen + +[glossary] +== Glossar + +== Verzeichnisse + +=== Abbildungsverzeichnis + +=== Tabellenverzeichnis + +<> <> diff --git a/java-quarkus/src/main/docker/Dockerfile.jvm b/java-quarkus/src/main/docker/Dockerfile.jvm new file mode 100644 index 0000000..476b644 --- /dev/null +++ b/java-quarkus/src/main/docker/Dockerfile.jvm @@ -0,0 +1,94 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./gradlew build +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/kontor-quarkus-jvm . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus-jvm +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus-jvm +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 + +ENV LANGUAGE='en_US:en' + + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=185 build/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 build/quarkus-app/*.jar /deployments/ +COPY --chown=185 build/quarkus-app/app/ /deployments/app/ +COPY --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 +ENV AB_JOLOKIA_OFF="" +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + diff --git a/java-quarkus/src/main/docker/Dockerfile.legacy-jar b/java-quarkus/src/main/docker/Dockerfile.legacy-jar new file mode 100644 index 0000000..8f309c6 --- /dev/null +++ b/java-quarkus/src/main/docker/Dockerfile.legacy-jar @@ -0,0 +1,90 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=legacy-jar +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/kontor-quarkus-legacy-jar . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus-legacy-jar +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus-legacy-jar +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 + +ENV LANGUAGE='en_US:en' + + +COPY build/lib/* /deployments/lib/ +COPY build/*-runner.jar /deployments/quarkus-run.jar + +EXPOSE 8080 +USER 185 +ENV AB_JOLOKIA_OFF="" +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" diff --git a/java-quarkus/src/main/docker/Dockerfile.native b/java-quarkus/src/main/docker/Dockerfile.native new file mode 100644 index 0000000..d5a28b2 --- /dev/null +++ b/java-quarkus/src/main/docker/Dockerfile.native @@ -0,0 +1,27 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=native +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t quarkus/kontor-quarkus . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus +# +### +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root build/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/java-quarkus/src/main/docker/Dockerfile.native-micro b/java-quarkus/src/main/docker/Dockerfile.native-micro new file mode 100644 index 0000000..55df6f9 --- /dev/null +++ b/java-quarkus/src/main/docker/Dockerfile.native-micro @@ -0,0 +1,30 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# It uses a micro base image, tuned for Quarkus native executables. +# It reduces the size of the resulting container image. +# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=native +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/kontor-quarkus . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus +# +### +FROM quay.io/quarkus/quarkus-micro-image:1.0 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root build/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/java-quarkus/src/main/java/de/thpeetz/kontor/comics/ComicsResource.java b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/ComicsResource.java new file mode 100644 index 0000000..f6518b9 --- /dev/null +++ b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/ComicsResource.java @@ -0,0 +1,95 @@ +package de.thpeetz.kontor.comics; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import de.thpeetz.kontor.comics.service.ArtistService; +import de.thpeetz.kontor.comics.service.ComicsService; +import de.thpeetz.kontor.comics.service.PublisherService; + +@Path("/kontor/comics") +public class ComicsResource { + + @Inject + transient ComicsService comicsService; + + @Inject + transient ArtistService artistService; + + @Inject + transient PublisherService publisherService; + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/") + public String showComicList() { + return comicsService.showComicList(); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/artist") + public String showArtistList() { + return artistService.showArtistList(); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/artist") + public String showArtistListJson() { + return artistService.showArtistList(); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/artist/view/{artist_id}") + public String showArtist(String artist_id) { + return artistService.showArtist(artist_id); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/artist/create") + public String showArtistCreation() { + return artistService.showArtistCreation(); + } + + @POST + @Produces(MediaType.TEXT_HTML) + @Path("/artist/create") + public String validateArtistDetails() { + return artistService.validateArtistDetails(); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/publisher") + public String showPublisherList() { + return publisherService.showPublisherList(); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/publisher/view/{publisher_id}") + public String showPublisher(String publisher_id) { + return publisherService.showPublisher(publisher_id); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/publisher/create") + public String showPublisherCreation() { + return publisherService.showPublisherCreation(); + } + + @POST + @Produces(MediaType.TEXT_HTML) + @Path("/publisher/create") + public String validatePublisherDetails() { + return publisherService.validatePublisherDetails(); + } +} diff --git a/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ArtistService.java b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ArtistService.java new file mode 100644 index 0000000..cdef9aa --- /dev/null +++ b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ArtistService.java @@ -0,0 +1,28 @@ +package de.thpeetz.kontor.comics.service; + +import javax.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class ArtistService { + + public String showArtistList() { + // TODO Auto-generated method stub + return null; + } + + public String showArtist(String artist_id) { + // TODO Auto-generated method stub + return null; + } + + public String showArtistCreation() { + // TODO Auto-generated method stub + return null; + } + + public String validateArtistDetails() { + // TODO Auto-generated method stub + return null; + } + +} diff --git a/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ComicsService.java b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ComicsService.java new file mode 100644 index 0000000..ee58e17 --- /dev/null +++ b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ComicsService.java @@ -0,0 +1,12 @@ +package de.thpeetz.kontor.comics.service; + +import javax.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class ComicsService { + + public String showComicList() { + return "comics"; + } + +} diff --git a/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/PublisherService.java b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/PublisherService.java new file mode 100644 index 0000000..d3414b1 --- /dev/null +++ b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/PublisherService.java @@ -0,0 +1,28 @@ +package de.thpeetz.kontor.comics.service; + +import javax.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class PublisherService { + + public String showPublisherList() { + // TODO Auto-generated method stub + return null; + } + + public String showPublisher(String publisher_id) { + // TODO Auto-generated method stub + return null; + } + + public String showPublisherCreation() { + // TODO Auto-generated method stub + return null; + } + + public String validatePublisherDetails() { + // TODO Auto-generated method stub + return null; + } + +} diff --git a/java-quarkus/src/main/resources/META-INF/resources/index.html b/java-quarkus/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000..609ae3a --- /dev/null +++ b/java-quarkus/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,284 @@ + + + + + kontor-quarkus - 1.0.0-SNAPSHOT + + + +
+
+
+ + + + + quarkus_logo_horizontal_rgb_1280px_reverse + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+

You just made a Quarkus application.

+

This page is served by Quarkus.

+ Visit the Dev UI +

This page: src/main/resources/META-INF/resources/index.html

+

App configuration: src/main/resources/application.properties

+

Static assets: src/main/resources/META-INF/resources/

+

Code: src/main/java

+

Generated starter code:

+
    +
  • + RESTEasy Reactive Easily start your Reactive RESTful Web Services +
    @Path: /hello +
    Related guide +
  • + +
+
+
+

Selected extensions

+
    +
  • RESTEasy Reactive (guide)
  • +
  • RESTEasy Reactive Jackson
  • +
+
Documentation
+

Practical step-by-step guides to help you achieve a specific goal. Use them to help get your work + done.

+
Set up your IDE
+

Everyone has a favorite IDE they like to use to code. Learn how to configure yours to maximize your + Quarkus productivity.

+
+
+
+ + diff --git a/java-quarkus/src/main/resources/application.properties b/java-quarkus/src/main/resources/application.properties new file mode 100644 index 0000000..21328a1 --- /dev/null +++ b/java-quarkus/src/main/resources/application.properties @@ -0,0 +1,3 @@ +# Your configuration properties +quarkus.http.test-port=8083 +quarkus.http.test-ssl-port=8446 diff --git a/java-quarkus/src/native-test/java/de/thpeetz/kontor/comics/ComicsResourceIT.java b/java-quarkus/src/native-test/java/de/thpeetz/kontor/comics/ComicsResourceIT.java new file mode 100644 index 0000000..230d951 --- /dev/null +++ b/java-quarkus/src/native-test/java/de/thpeetz/kontor/comics/ComicsResourceIT.java @@ -0,0 +1,8 @@ +package de.thpeetz.kontor.comics; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class ComicsResourceIT extends ComicsResourceTest { + +} diff --git a/java-quarkus/src/test/java/de/thpeetz/kontor/comics/ComicsResourceTest.java b/java-quarkus/src/test/java/de/thpeetz/kontor/comics/ComicsResourceTest.java new file mode 100644 index 0000000..25dcd49 --- /dev/null +++ b/java-quarkus/src/test/java/de/thpeetz/kontor/comics/ComicsResourceTest.java @@ -0,0 +1,19 @@ +package de.thpeetz.kontor.comics; + +import org.junit.jupiter.api.Test; +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class ComicsResourceTest { + + @Test + void testComicsEndpoint() { + given().when().get("/kontor/comics") + .then() + .statusCode(200) + .body(is("comics")); + } +} From 2ae11e24eff04d6938234a00f609a4e4bd1a8ce1 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Fri, 10 Jan 2025 17:39:54 +0100 Subject: [PATCH 20/91] add sqlalchemy as orm tool --- gui/comic_model.py | 89 -------------------------- gui/data.py | 79 ----------------------- gui/database/__init__.py | 134 +++++++++++++++++++++++++++++++++++++++ gui/database/base.py | 7 ++ gui/database/comic.py | 40 ++++++++++++ gui/database/media.py | 25 ++++++++ gui/database/metadata.py | 49 ++++++++++++++ gui/kontor.py | 3 +- gui/model_config.py | 7 +- gui/table_model.py | 20 ++++++ 10 files changed, 281 insertions(+), 172 deletions(-) delete mode 100644 gui/comic_model.py delete mode 100644 gui/data.py create mode 100644 gui/database/__init__.py create mode 100644 gui/database/base.py create mode 100644 gui/database/comic.py create mode 100644 gui/database/media.py create mode 100644 gui/database/metadata.py diff --git a/gui/comic_model.py b/gui/comic_model.py deleted file mode 100644 index 2472670..0000000 --- a/gui/comic_model.py +++ /dev/null @@ -1,89 +0,0 @@ -from datetime import datetime - -import mariadb -from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt -from PySide6.QtGui import QColor - - -class ComicTableModel(QAbstractTableModel): - - def __init__(self, db_config, main_window): - super().__init__() - self.main_window = main_window - self._data = [] - self.status_bar = main_window.statusBar - self.mariadb_conn = mariadb.connect( - host=db_config['mariadb']['host'], - port=db_config['mariadb']['port'], - user=db_config['mariadb']['user'], - password=db_config['mariadb']['password'], - database=db_config['mariadb']['database'] - ) - self.refresh() - - def refresh(self): - data = [] - cursor = self.mariadb_conn.cursor() - cursor.execute("SELECT id, created_date, last_modified_date, title, publisher_id FROM comic") - rows = cursor.fetchall() - for row in rows: - data.append(list(row)) - self.status_bar.showMessage(f"{len(rows)} Einträge geladen", 3000) - self._data = data - - def rowCount(self, parent=QModelIndex()): - # The length of the outer list. - return len(self._data) - - def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): - if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: - match col: - case 0: - return "ID" - case 1: - return "Created" - case 2: - return "Updated" - case 3: - return "Title" - case 4: - return "Verlag" - if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: - return str(col + 1) - - def data(self, index, role=Qt.ItemDataRole.DisplayRole): - value = self._data[index.row()][index.column()] - if role == Qt.ItemDataRole.DisplayRole: - if isinstance(value, datetime): - return value.strftime("%Y-%m-%d %M:%M:%S") - if isinstance(value, str): - return value - if isinstance(value, bytes): - if value == b'\x01': - return "True" - return "False" - return value - if role == Qt.ItemDataRole.DecorationRole: - if isinstance(value, bytes): - # print('{}: {}'.format(value, type(value))) - if value == b'\x01': - return self.main_window.tick - else: - return self.main_window.cross - - - def columnCount(self, index=QModelIndex()): - # The following takes the first sub-list, and returns - # the length (only works if all rows are an equal length) - return len(self._data[0]) - - def setData(self, index, value, role=Qt.ItemDataRole.EditRole): - if role == Qt.ItemDataRole.EditRole: - self._data[index.row()][index.column()] = value - if role == Qt.ItemDataRole.CheckStateRole: - checked = value == Qt.CheckState.Checked - self._data[index.row()][index.column()] = checked - return True - - def flags(self, index): - return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate diff --git a/gui/data.py b/gui/data.py deleted file mode 100644 index bb4436c..0000000 --- a/gui/data.py +++ /dev/null @@ -1,79 +0,0 @@ -import mariadb - - -class KontorDB: - - def __init__(self, db_config): - self.db_conn = mariadb.connect( - host=db_config['mariadb']['host'], - port=db_config['mariadb']['port'], - user=db_config['mariadb']['user'], - password=db_config['mariadb']['password'], - database=db_config['mariadb']['database'] - ) - - def get_table_id(self, table_name): - cursor = self.db_conn.cursor() - cursor.execute("SELECT id, created_date, last_modified_date FROM meta_data_table WHERE table_name=?", (table_name, )) - row = cursor.fetchone() - cursor.close() - return row[0] - - def get_table_names(self) -> list: - tables_names = [] - cursor = self.db_conn.cursor() - cursor.execute("SELECT id, table_name from meta_data_table") - rows = cursor.fetchall() - for (_, table_name) in rows: - tables_names.append(table_name) - cursor.close() - return tables_names - - def get_column_meta_data(self, table_id): - cursor = self.db_conn.cursor() - meta_data = {} - cursor.execute("SELECT column_name, column_order, column_label FROM meta_data_column WHERE table_id=? AND is_shown is true ORDER bY column_order", (table_id, )) - rows = cursor.fetchall() - order = 0 - for (column_name, column_order, column_label) in rows: - meta_data[order] = { 'column': column_name, 'label': column_label, 'order': column_order} - order += 1 - cursor.close() - # print(f"retrieved {len(rows)} columns, set {len(meta_data)} headers") - return meta_data - - def get_filters(self, table_id): - cursor = self.db_conn.cursor() - filters = {} - cursor.execute("SELECT column_name, filter_label from meta_data_column WHERE table_id=? AND show_filter is true", (table_id, )) - rows = cursor.fetchall() - for row in rows: - filters[row[0]] = {'label': row[1], 'widget': None} - cursor.close() - # print(f"retrieved {len(rows)} filters: {filters}") - return filters - - def get_data(self, table_name: str, columns: dict, where_clause: str) -> list: - data = [] - cursor = self.db_conn.cursor() - cursor.execute(self.get_statement(table_name, columns, where_clause)) - rows = cursor.fetchall() - print(len(rows)) - for row in rows: - # print(f"KontorDB.get_data: {row}") - data.append(list(row)) - cursor.close() - print(f"KontorDB.getData: return {len(data)}") - return data - - def get_statement(self, table: str, header: dict, where_clause): - columns = "" - for index, column in header.items(): - if index > 0: - columns += ", " - columns += column['column'] - if len(columns) == 0: - columns = "*" - statement = f"SELECT {columns} FROM {table} {where_clause}" - print(f"{statement=}") - return statement diff --git a/gui/database/__init__.py b/gui/database/__init__.py new file mode 100644 index 0000000..122959e --- /dev/null +++ b/gui/database/__init__.py @@ -0,0 +1,134 @@ +import mariadb +from sqlalchemy import create_engine, text, MetaData, join +from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker + +from database.base import Base +from database.comic import Comic +from database.metadata import MetaDataTable, MetaDataColumn + + +class KontorDB: + + def __init__(self, db_config): + self.db_conn = mariadb.connect( + host=db_config['mariadb']['host'], + port=db_config['mariadb']['port'], + user=db_config['mariadb']['user'], + password=db_config['mariadb']['password'], + database=db_config['mariadb']['database'] + ) + connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}' + .format( + db_config['mariadb']['user'], + db_config['mariadb']['password'], + db_config['mariadb']['host'], + db_config['mariadb']['port'], + db_config['mariadb']['database'])) + # engine = create_engine(connect_string, echo=True) + engine = create_engine(connect_string) + Base.metadata.create_all(bind=engine) + __session__ = sessionmaker(bind=engine) + self.session = __session__() + + def get_table_id(self, table_name): + cursor = self.db_conn.cursor() + cursor.execute("SELECT id, created_date, last_modified_date FROM meta_data_table WHERE table_name=?", + (table_name,)) + row = cursor.fetchone() + cursor.close() + # table_ids = self.session.query(MetaDataTable).filter(MetaDataTable.table_name == table_name).all() + # print(type(table_ids)) + return row[0] + + def get_table_names(self) -> list: + tables_names = [] + cursor = self.db_conn.cursor() + cursor.execute("SELECT id, table_name from meta_data_table") + rows = cursor.fetchall().all + for row in rows: + print(row) + for (_, table_name) in rows: + tables_names.append(table_name) + cursor.close() + tables = self.session.query(MetaDataTable).all() + for table in tables: + print(table) + return tables_names + + def get_column_meta_data(self, table_id: str, table_name: str): + cursor = self.db_conn.cursor() + meta_data = {} + cursor.execute( + "SELECT column_name, column_order, column_label FROM meta_data_column WHERE table_id=? AND is_shown is true ORDER bY column_order", + (table_id,)) + rows = cursor.fetchall() + order = 0 + for (column_name, column_order, column_label) in rows: + meta_data[order] = {'column': column_name, 'label': column_label, 'order': column_order} + order += 1 + cursor.close() + columns = (self.session.query(MetaDataColumn). + join(MetaDataTable, MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name). + filter(MetaDataColumn.is_shown is True). + order_by(MetaDataColumn.column_order).all()) + print(columns) + for column in columns: + print(column) + result = repr(column) + if result is not None: + print(result) + # print(f"retrieved {len(rows)} columns, set {len(meta_data)} headers") + return meta_data + + def get_filters(self, table_id): + cursor = self.db_conn.cursor() + filters = {} + cursor.execute( + "SELECT column_name, filter_label from meta_data_column WHERE table_id=? AND show_filter is true", + (table_id,)) + rows = cursor.fetchall() + for row in rows: + filters[row[0]] = {'label': row[1], 'widget': None} + cursor.close() + # print(f"retrieved {len(rows)} filters: {filters}") + return filters + + def get_data(self, table_name: str, columns: dict, where_clause: str) -> list: + data = [] + cursor = self.db_conn.cursor() + cursor.execute(self.get_statement(table_name, columns, where_clause)) + rows = cursor.fetchall() + print(len(rows)) + for row in rows: + # print(f"KontorDB.get_data: {row}") + data.append(list(row)) + cursor.close() + print(f"KontorDB.getData: return {len(data)}") + if table_name == 'comic' and len(where_clause) == 0: + data.clear() + comics = self.session.query(Comic).all() + for item in comics: + # print(item) + row = [] + for order in columns.keys(): + column_name = columns[order]['column'] + if column_name == 'publisher_id': + row.append(item.publisher.name) + else: + row.append(getattr(item, column_name)) + # print(repr(row)) + data.append(row) + return data + + def get_statement(self, table: str, header: dict, where_clause): + columns = "" + for index, column in header.items(): + if index > 0: + columns += ", " + columns += column['column'] + if len(columns) == 0: + columns = "*" + statement = f"SELECT {columns} FROM {table} {where_clause}" + print(f"{statement=}") + return statement diff --git a/gui/database/base.py b/gui/database/base.py new file mode 100644 index 0000000..fb74a49 --- /dev/null +++ b/gui/database/base.py @@ -0,0 +1,7 @@ +from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker + + +class Base(DeclarativeBase): + pass + + diff --git a/gui/database/comic.py b/gui/database/comic.py new file mode 100644 index 0000000..c17244e --- /dev/null +++ b/gui/database/comic.py @@ -0,0 +1,40 @@ +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from database.base import Base + + +class Publisher(Base): + __tablename__ = "publisher" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255)) + comics = relationship("Comic") + + def __repr__(self): + return f'Publisher({self.id} {self.name})' + + def __str__(self): + return self.__repr__() + + +class Comic(Base): + __tablename__ = 'comic' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + title = Column(String(length=255)) + publisher_id = Column(String, ForeignKey('publisher.id')) + publisher = relationship("Publisher", back_populates="comics") + current_order = Column(BIT(1)) + completed = Column(BIT(1)) + + def __repr__(self): + return f'Comic({self.id} {self.version} {self.title} {self.publisher.name})' + + def __str__(self): + return f'{self.title}({self.id})' diff --git a/gui/database/media.py b/gui/database/media.py new file mode 100644 index 0000000..ebebc57 --- /dev/null +++ b/gui/database/media.py @@ -0,0 +1,25 @@ +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from database.base import Base + + +class MediaFile(Base): + __tablename__ = 'media_file' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + cloud_link = Column(String(255)) + file_name = Column(String(255)) + path = Column(String(255)) + review = Column(Boolean, default=True) + title = Column(String(255)) + url = Column(String(255)) + should_download = Column(Boolean, default=True) + + def __repr__(self): + return f'MediaFile({self.id} {self.title} {self.title})' + + def __str__(self): + return f'{self.title}({self.id})' diff --git a/gui/database/metadata.py b/gui/database/metadata.py new file mode 100644 index 0000000..8a59633 --- /dev/null +++ b/gui/database/metadata.py @@ -0,0 +1,49 @@ +from sqlalchemy import Column, String, ForeignKey, DateTime, Integer, Boolean +from sqlalchemy.orm import relationship + +from database import Base + + +class MetaDataTable(Base): + __tablename__ = 'meta_data_table' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + table_name = Column(String(255), unique=True) + table_columns = relationship("MetaDataColumn") + + def __repr__(self): + print(f'MetaDataTable({self.id} {self.table_name})') + + def __str__(self): + print(f'{self.table_name}({self.id})') + + +class MetaDataColumn(Base): + __tablename__ = 'meta_data_column' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + column_modifier = Column(String(255), nullable=True) + column_name = Column(String(255)) + column_order = Column(Integer) + column_sync_name = Column(String(255)) + column_type = Column(String(255)) + table_id = Column(String, ForeignKey('meta_data_table.id')) + table = relationship("MetaDataTable", back_populates="table_columns") + column_label = Column(String(255)) + filter_label = Column(String(255)) + is_shown = Column(Boolean) + show_filter = Column(Boolean) + + def __repr__(self): + if self.column_name is None: + print(f'MetaDataColumn({self.id} {self.table.table_name}.__)') + else: + print(f'MetaDataColumn({self.id} {self.table.table_name}.{self.column_name})') + + def __str__(self): + print(f'{self.column_name}({self.id})') + diff --git a/gui/kontor.py b/gui/kontor.py index 30bb3bd..6356bd2 100644 --- a/gui/kontor.py +++ b/gui/kontor.py @@ -10,9 +10,8 @@ from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidg from PySide6.QtWidgets import QApplication, QLabel, QMainWindow from platformdirs import PlatformDirs -from comic_model import ComicTableModel +from database import KontorDB from dialogs import ExportKontorDialog, ImportKontorDialog -from data import KontorDB from model_config import KontorModelConfig from table_model import KontorTableModel diff --git a/gui/model_config.py b/gui/model_config.py index ba3e510..abff7da 100644 --- a/gui/model_config.py +++ b/gui/model_config.py @@ -1,7 +1,8 @@ import mariadb from PySide6.QtWidgets import QHBoxLayout, QCheckBox -from data import KontorDB +from database import KontorDB +from database.comic import Comic class KontorModelConfig: @@ -23,7 +24,7 @@ class KontorModelConfig: def get_table_config(self): if self._table_id is None: self.get_table_id() - self.header = self.kontor_db.get_column_meta_data(self._table_id) + self.header = self.kontor_db.get_column_meta_data(self._table_id, self._table) self.filter = self.kontor_db.get_filters(self._table_id) def get_filter(self) -> str: @@ -44,6 +45,8 @@ class KontorModelConfig: def get_data(self) -> list: data = self.kontor_db.get_data(self._table, self.header, self.get_filter()) print(f"KontorModelConfig.get_data: {len(data)}") + # comics = self.kontor_db.session.query(Comic).all() + # print(f'{len(comics)} Comics loaded') return data def get_filter_layout(self) -> QHBoxLayout: diff --git a/gui/table_model.py b/gui/table_model.py index a28e1cc..2af11c9 100644 --- a/gui/table_model.py +++ b/gui/table_model.py @@ -56,6 +56,16 @@ class KontorTableModel(QAbstractTableModel): return self._main_window.tick else: return self._main_window.cross + if isinstance(value, int): + if value == 1: + return self._main_window.tick + else: + return self._main_window.cross + if isinstance(value, bool): + if value: + return self._main_window.tick + else: + return self._main_window.cross return str(value) if role == Qt.ItemDataRole.DecorationRole: if isinstance(value, bytes): @@ -64,6 +74,16 @@ class KontorTableModel(QAbstractTableModel): return self._main_window.tick else: return self._main_window.cross + if isinstance(value, int): + if value == 1: + return self._main_window.tick + else: + return self._main_window.cross + if isinstance(value, bool): + if value: + return self._main_window.tick + else: + return self._main_window.cross def columnCount(self, index=QModelIndex()): # The following takes the first sub-list, and returns From 8f2e99195a58c55ef14d672a4eeb85c066866cfe Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sat, 11 Jan 2025 01:54:05 +0100 Subject: [PATCH 21/91] reorganize files for Qt application --- gui/resources.py | 7 -- qt/.gitignore | 2 + {gui => qt}/database/__init__.py | 50 ++---------- {gui => qt}/database/base.py | 0 {gui => qt}/database/comic.py | 0 {gui => qt}/database/media.py | 0 {gui => qt}/database/metadata.py | 11 ++- qt/gui/__init__.py | 0 {gui => qt/gui}/dialogs.py | 0 gui/kontor.py => qt/gui/main_window.py | 27 +------ {gui => qt/gui}/model_config.py | 1 - {gui => qt/gui}/table_model.py | 2 +- qt/kontor.py | 21 +++++ qt/pysidedeploy.spec | 98 ++++++++++++++++++++++++ {gui => qt}/res/application-export.png | Bin {gui => qt}/res/application-import.png | Bin {gui => qt}/res/arrow-circle-double.png | Bin {gui => qt}/res/cross.png | Bin {gui => qt}/res/tick.png | Bin 19 files changed, 139 insertions(+), 80 deletions(-) delete mode 100644 gui/resources.py create mode 100644 qt/.gitignore rename {gui => qt}/database/__init__.py (63%) rename {gui => qt}/database/base.py (100%) rename {gui => qt}/database/comic.py (100%) rename {gui => qt}/database/media.py (100%) rename {gui => qt}/database/metadata.py (80%) create mode 100644 qt/gui/__init__.py rename {gui => qt/gui}/dialogs.py (100%) rename gui/kontor.py => qt/gui/main_window.py (88%) rename {gui => qt/gui}/model_config.py (98%) rename {gui => qt/gui}/table_model.py (98%) create mode 100644 qt/kontor.py create mode 100755 qt/pysidedeploy.spec rename {gui => qt}/res/application-export.png (100%) rename {gui => qt}/res/application-import.png (100%) rename {gui => qt}/res/arrow-circle-double.png (100%) rename {gui => qt}/res/cross.png (100%) rename {gui => qt}/res/tick.png (100%) diff --git a/gui/resources.py b/gui/resources.py deleted file mode 100644 index c135d07..0000000 --- a/gui/resources.py +++ /dev/null @@ -1,7 +0,0 @@ -from PySide6.QtGui import QIcon - -tick = QIcon('tick.png') -cross = QIcon('cross.png') -import_icon = QIcon("application-import.png") -export_icon = QIcon("application-export.png") -circle_icon = QIcon("arrow-circle-double.png") diff --git a/qt/.gitignore b/qt/.gitignore new file mode 100644 index 0000000..38b154f --- /dev/null +++ b/qt/.gitignore @@ -0,0 +1,2 @@ +deployment/ +kontor.bin diff --git a/gui/database/__init__.py b/qt/database/__init__.py similarity index 63% rename from gui/database/__init__.py rename to qt/database/__init__.py index 122959e..c6d21a1 100644 --- a/gui/database/__init__.py +++ b/qt/database/__init__.py @@ -1,5 +1,5 @@ import mariadb -from sqlalchemy import create_engine, text, MetaData, join +from sqlalchemy import create_engine, select, text, MetaData, join from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker from database.base import Base @@ -31,54 +31,20 @@ class KontorDB: self.session = __session__() def get_table_id(self, table_name): - cursor = self.db_conn.cursor() - cursor.execute("SELECT id, created_date, last_modified_date FROM meta_data_table WHERE table_name=?", - (table_name,)) - row = cursor.fetchone() - cursor.close() - # table_ids = self.session.query(MetaDataTable).filter(MetaDataTable.table_name == table_name).all() - # print(type(table_ids)) - return row[0] + result = self.session.execute(select(MetaDataTable.id).where(MetaDataTable.table_name == table_name)).scalar() + return result def get_table_names(self) -> list: - tables_names = [] - cursor = self.db_conn.cursor() - cursor.execute("SELECT id, table_name from meta_data_table") - rows = cursor.fetchall().all - for row in rows: - print(row) - for (_, table_name) in rows: - tables_names.append(table_name) - cursor.close() tables = self.session.query(MetaDataTable).all() - for table in tables: - print(table) - return tables_names + result = [table.table_name for table in tables] + return result - def get_column_meta_data(self, table_id: str, table_name: str): - cursor = self.db_conn.cursor() + def get_column_meta_data(self, table_id: str, table_name: str) -> dict: meta_data = {} - cursor.execute( - "SELECT column_name, column_order, column_label FROM meta_data_column WHERE table_id=? AND is_shown is true ORDER bY column_order", - (table_id,)) - rows = cursor.fetchall() order = 0 - for (column_name, column_order, column_label) in rows: - meta_data[order] = {'column': column_name, 'label': column_label, 'order': column_order} + for (_, column) in self.session.query(MetaDataTable, MetaDataColumn).filter(MetaDataTable.id == MetaDataColumn.table_id).filter(MetaDataTable.table_name == table_name).filter(MetaDataColumn.is_shown == 1).all(): + meta_data[order] = {'column': column.column_name, 'label': column.column_label, 'order': column.column_order} order += 1 - cursor.close() - columns = (self.session.query(MetaDataColumn). - join(MetaDataTable, MetaDataTable.id == MetaDataColumn.table_id). - filter(MetaDataTable.table_name == table_name). - filter(MetaDataColumn.is_shown is True). - order_by(MetaDataColumn.column_order).all()) - print(columns) - for column in columns: - print(column) - result = repr(column) - if result is not None: - print(result) - # print(f"retrieved {len(rows)} columns, set {len(meta_data)} headers") return meta_data def get_filters(self, table_id): diff --git a/gui/database/base.py b/qt/database/base.py similarity index 100% rename from gui/database/base.py rename to qt/database/base.py diff --git a/gui/database/comic.py b/qt/database/comic.py similarity index 100% rename from gui/database/comic.py rename to qt/database/comic.py diff --git a/gui/database/media.py b/qt/database/media.py similarity index 100% rename from gui/database/media.py rename to qt/database/media.py diff --git a/gui/database/metadata.py b/qt/database/metadata.py similarity index 80% rename from gui/database/metadata.py rename to qt/database/metadata.py index 8a59633..2fd0af7 100644 --- a/gui/database/metadata.py +++ b/qt/database/metadata.py @@ -14,11 +14,10 @@ class MetaDataTable(Base): table_columns = relationship("MetaDataColumn") def __repr__(self): - print(f'MetaDataTable({self.id} {self.table_name})') + return f'MetaDataTable({self.id} {self.table_name})' def __str__(self): - print(f'{self.table_name}({self.id})') - + return f'{self.table_name}({self.id})' class MetaDataColumn(Base): __tablename__ = 'meta_data_column' @@ -40,10 +39,10 @@ class MetaDataColumn(Base): def __repr__(self): if self.column_name is None: - print(f'MetaDataColumn({self.id} {self.table.table_name}.__)') + return f'MetaDataColumn({self.id} {self.table.table_name}.__)' else: - print(f'MetaDataColumn({self.id} {self.table.table_name}.{self.column_name})') + return f'MetaDataColumn({self.id} {self.table.table_name}.{self.column_name})' def __str__(self): - print(f'{self.column_name}({self.id})') + return f'{self.column_name}({self.id})' diff --git a/qt/gui/__init__.py b/qt/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gui/dialogs.py b/qt/gui/dialogs.py similarity index 100% rename from gui/dialogs.py rename to qt/gui/dialogs.py diff --git a/gui/kontor.py b/qt/gui/main_window.py similarity index 88% rename from gui/kontor.py rename to qt/gui/main_window.py index 6356bd2..a652906 100644 --- a/gui/kontor.py +++ b/qt/gui/main_window.py @@ -1,19 +1,11 @@ -""" -PyQT6 GUI for Kontor -""" -import sys -from pathlib import Path - -import yaml from PySide6.QtGui import QAction, QIcon from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView -from PySide6.QtWidgets import QApplication, QLabel, QMainWindow -from platformdirs import PlatformDirs +from PySide6.QtWidgets import QLabel, QMainWindow from database import KontorDB -from dialogs import ExportKontorDialog, ImportKontorDialog -from model_config import KontorModelConfig -from table_model import KontorTableModel +from gui.dialogs import ExportKontorDialog, ImportKontorDialog +from gui.model_config import KontorModelConfig +from gui.table_model import KontorTableModel class MainWindow(QMainWindow): @@ -137,14 +129,3 @@ class MainWindow(QMainWindow): layout.addWidget(table_view) model.refresh() return data_tab - - -if __name__ == '__main__': - app = QApplication(sys.argv) - dirs = PlatformDirs("kontor") - database_config = Path(dirs.user_config_dir, 'database-config.yaml') - with open(database_config, 'rt') as f: - db_config = yaml.safe_load(f.read()) - window = MainWindow(db_config) - window.show() - app.exec() diff --git a/gui/model_config.py b/qt/gui/model_config.py similarity index 98% rename from gui/model_config.py rename to qt/gui/model_config.py index abff7da..01d3805 100644 --- a/gui/model_config.py +++ b/qt/gui/model_config.py @@ -2,7 +2,6 @@ import mariadb from PySide6.QtWidgets import QHBoxLayout, QCheckBox from database import KontorDB -from database.comic import Comic class KontorModelConfig: diff --git a/gui/table_model.py b/qt/gui/table_model.py similarity index 98% rename from gui/table_model.py rename to qt/gui/table_model.py index 2af11c9..5e80c3b 100644 --- a/gui/table_model.py +++ b/qt/gui/table_model.py @@ -3,7 +3,7 @@ from datetime import datetime from PySide6.QtCore import QAbstractTableModel, QModelIndex from PySide6.QtGui import Qt -from model_config import KontorModelConfig +from gui.model_config import KontorModelConfig class KontorTableModel(QAbstractTableModel): diff --git a/qt/kontor.py b/qt/kontor.py new file mode 100644 index 0000000..2eed73b --- /dev/null +++ b/qt/kontor.py @@ -0,0 +1,21 @@ +""" +PyQT6 GUI for Kontor +""" +import sys +from pathlib import Path +from platformdirs import PlatformDirs +from PySide6.QtWidgets import QApplication +import yaml + +from gui.main_window import MainWindow + + +if __name__ == '__main__': + app = QApplication(sys.argv) + dirs = PlatformDirs("kontor") + database_config = Path(dirs.user_config_dir, 'database-config.yaml') + with open(database_config, 'rt') as f: + db_config = yaml.safe_load(f.read()) + window = MainWindow(db_config) + window.show() + app.exec() diff --git a/qt/pysidedeploy.spec b/qt/pysidedeploy.spec new file mode 100755 index 0000000..8e65745 --- /dev/null +++ b/qt/pysidedeploy.spec @@ -0,0 +1,98 @@ +[app] + +# title of your application +title = kontor + +# project directory. the general assumption is that project_dir is the parent directory +# of input_file +project_dir = /home/tpeetz/projects/kontor/qt + +# source file path +input_file = /home/tpeetz/projects/kontor/qt/kontor.py + +# directory where exec is stored +exec_directory = . + +# path to .pyproject project file +project_file = + +# application icon +icon = /usr/local/lib/python3.11/dist-packages/PySide6/scripts/deploy_lib/pyside_icon.jpg + +[python] + +# python path +python_path = /usr/bin/python3 + +# python packages to install +packages = Nuitka==2.4.8 + +# buildozer = for deploying Android application +android_packages = buildozer==1.5.0,cython==0.29.33 + +[qt] + +# comma separated path to qml files required +# normally all the qml files required by the project are added automatically +qml_files = + +# excluded qml plugin binaries +excluded_qml_plugins = + +# qt modules used. comma separated +modules = Gui,DBus,Core,Widgets + +# qt plugins used by the application +plugins = platformthemes,imageformats,platforms,generic,iconengines,egldeviceintegrations,styles,xcbglintegrations,platforms/darwin,platforminputcontexts,accessiblebridge + +[android] + +# path to pyside wheel +wheel_pyside = + +# path to shiboken wheel +wheel_shiboken = + +# plugins to be copied to libs folder of the packaged application. comma separated +plugins = + +[nuitka] + +# usage description for permissions requested by the app as found in the info.plist file +# of the app bundle +# eg = extra_args = --show-modules --follow-stdlib +macos.permissions = + +# mode of using nuitka. accepts standalone or onefile. default is onefile. +mode = onefile + +# (str) specify any extra nuitka arguments +extra_args = --quiet --noinclude-qt-translations + +[buildozer] + +# build mode +# possible options = [release, debug] +# release creates an aab, while debug creates an apk +mode = debug + +# contrains path to pyside6 and shiboken6 recipe dir +recipe_dir = + +# path to extra qt android jars to be loaded by the application +jars_dir = + +# if empty uses default ndk path downloaded by buildozer +ndk_path = + +# if empty uses default sdk path downloaded by buildozer +sdk_path = + +# other libraries to be loaded. comma separated. +# loaded at app startup +local_libs = + +# architecture of deployed platform +# possible values = ["aarch64", "armv7a", "i686", "x86_64"] +arch = + diff --git a/gui/res/application-export.png b/qt/res/application-export.png similarity index 100% rename from gui/res/application-export.png rename to qt/res/application-export.png diff --git a/gui/res/application-import.png b/qt/res/application-import.png similarity index 100% rename from gui/res/application-import.png rename to qt/res/application-import.png diff --git a/gui/res/arrow-circle-double.png b/qt/res/arrow-circle-double.png similarity index 100% rename from gui/res/arrow-circle-double.png rename to qt/res/arrow-circle-double.png diff --git a/gui/res/cross.png b/qt/res/cross.png similarity index 100% rename from gui/res/cross.png rename to qt/res/cross.png diff --git a/gui/res/tick.png b/qt/res/tick.png similarity index 100% rename from gui/res/tick.png rename to qt/res/tick.png From a3652cc9b8cf471e1a01ff340a28c42241e1830d Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sat, 11 Jan 2025 19:00:58 +0100 Subject: [PATCH 22/91] add tysc schema --- qt/database/base.py | 2 +- qt/database/media.py | 6 +- qt/database/metadata.py | 6 +- qt/database/tysc.py | 68 +++++++++++++++++++ .../de/thpeetz/kontor/tysc/data/Team.java | 1 - 5 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 qt/database/tysc.py diff --git a/qt/database/base.py b/qt/database/base.py index fb74a49..9339e51 100644 --- a/qt/database/base.py +++ b/qt/database/base.py @@ -1,7 +1,7 @@ +from sqlalchemy import Column, String, DateTime, Integer from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker class Base(DeclarativeBase): pass - diff --git a/qt/database/media.py b/qt/database/media.py index ebebc57..621fdce 100644 --- a/qt/database/media.py +++ b/qt/database/media.py @@ -1,5 +1,5 @@ -from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String -from sqlalchemy.orm import relationship +from sqlalchemy import Boolean, Column, DateTime, Integer, String +from sqlalchemy.dialects.mysql import BIT from database.base import Base @@ -13,7 +13,7 @@ class MediaFile(Base): cloud_link = Column(String(255)) file_name = Column(String(255)) path = Column(String(255)) - review = Column(Boolean, default=True) + review = Column(BIT(1), default=True) title = Column(String(255)) url = Column(String(255)) should_download = Column(Boolean, default=True) diff --git a/qt/database/metadata.py b/qt/database/metadata.py index 2fd0af7..e1b9084 100644 --- a/qt/database/metadata.py +++ b/qt/database/metadata.py @@ -1,4 +1,5 @@ from sqlalchemy import Column, String, ForeignKey, DateTime, Integer, Boolean +from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import relationship from database import Base @@ -34,8 +35,8 @@ class MetaDataColumn(Base): table = relationship("MetaDataTable", back_populates="table_columns") column_label = Column(String(255)) filter_label = Column(String(255)) - is_shown = Column(Boolean) - show_filter = Column(Boolean) + is_shown = Column(BIT(1)) + show_filter = Column(BIT(1)) def __repr__(self): if self.column_name is None: @@ -45,4 +46,3 @@ class MetaDataColumn(Base): def __str__(self): return f'{self.column_name}({self.id})' - diff --git a/qt/database/tysc.py b/qt/database/tysc.py new file mode 100644 index 0000000..e78ff17 --- /dev/null +++ b/qt/database/tysc.py @@ -0,0 +1,68 @@ +from sqlalchemy import Boolean, Column, DateTime, Integer, String, ForeignKey +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from database.base import Base + + +class Sport(Base): + __tablename__ = "sport" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255)) + teams = relationship("Team") + positions = relationship("FieldPosition") + + +class Team(Base): + __tablename__ = "team" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255)) + short_name = Column(String(255)) + sport_id = Column(String, ForeignKey("sport.id")) + sport = relationship("Sport", back_populates="positions") + roosters = relationship("Rooster") + + +class FieldPosition(Base): + __tablename__ = "field_position" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255)) + short_name = Column(String(255)) + sport_id = Column(String, ForeignKey("sport.id")) + sport = relationship("Sport", back_populates="positions") + roosters = relationship("Rooster") + + +class Player(Base): + __tablename__ = "player" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + first_name = Column(String(255)) + last_name = Column(String(255)) + roosters = relationship("Rooster") + + +class Rooster(Base): + __tablename__ = "rooster" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + year = Column(Integer) + team_id = Column(String, ForeignKey("team.id")) + team = relationship("Team", back_populates="roosters") + player_id = Column(String, ForeignKey("player.id")) + player = relationship("Player", back_populates="roosters") + position_id = Column(String, ForeignKey("field_position.id")) + position = relationship("roosters") diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Team.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Team.java index b61fdd2..353da78 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Team.java +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Team.java @@ -1,7 +1,6 @@ package de.thpeetz.kontor.tysc.data; import java.util.List; -import java.util.Objects; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; From bf14c9a020c466987d67cbb680f52302bc703a2f Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 12 Jan 2025 02:41:40 +0100 Subject: [PATCH 23/91] extend MetaDataColumn by adding column for name of column for referenced tables --- qt/database/__init__.py | 13 ++- qt/database/comic.py | 96 ++++++++++++++++++- qt/database/metadata.py | 1 + qt/database/tysc.py | 89 ++++++++++++++--- qt/gui/main_window.py | 4 + qt/gui/model_config.py | 2 +- .../kontor/admin/SetupModuleAdmin.java | 6 +- .../kontor/admin/data/MetaDataColumn.java | 2 + .../admin/services/MetaDataService.java | 9 ++ .../kontor/admin/views/MetaDataForm.java | 2 + .../kontor/admin/views/MetaDataView.java | 4 +- 11 files changed, 203 insertions(+), 25 deletions(-) diff --git a/qt/database/__init__.py b/qt/database/__init__.py index c6d21a1..c0191cc 100644 --- a/qt/database/__init__.py +++ b/qt/database/__init__.py @@ -43,7 +43,7 @@ class KontorDB: meta_data = {} order = 0 for (_, column) in self.session.query(MetaDataTable, MetaDataColumn).filter(MetaDataTable.id == MetaDataColumn.table_id).filter(MetaDataTable.table_name == table_name).filter(MetaDataColumn.is_shown == 1).all(): - meta_data[order] = {'column': column.column_name, 'label': column.column_label, 'order': column.column_order} + meta_data[order] = {'column': column.column_name, 'label': column.column_label, 'order': column.column_order, 'ref_column': column.ref_column} order += 1 return meta_data @@ -70,7 +70,7 @@ class KontorDB: # print(f"KontorDB.get_data: {row}") data.append(list(row)) cursor.close() - print(f"KontorDB.getData: return {len(data)}") + # print(f"KontorDB.getData: return {len(data)}") if table_name == 'comic' and len(where_clause) == 0: data.clear() comics = self.session.query(Comic).all() @@ -79,8 +79,13 @@ class KontorDB: row = [] for order in columns.keys(): column_name = columns[order]['column'] - if column_name == 'publisher_id': - row.append(item.publisher.name) + if str(column_name).endswith("_id"): + ref_table = column_name[:-3] + # print(f"{ref_table=}") + ref = getattr(item, ref_table) + value = getattr(ref, "name") + # print(f"{value=}") + row.append(value) else: row.append(getattr(item, column_name)) # print(repr(row)) diff --git a/qt/database/comic.py b/qt/database/comic.py index c17244e..dc4204f 100644 --- a/qt/database/comic.py +++ b/qt/database/comic.py @@ -11,7 +11,7 @@ class Publisher(Base): created_date = Column(DateTime) last_modified_date = Column(DateTime) version = Column(Integer) - name = Column(String(length=255)) + name = Column(String(length=255), unique=True) comics = relationship("Comic") def __repr__(self): @@ -27,14 +27,104 @@ class Comic(Base): created_date = Column(DateTime) last_modified_date = Column(DateTime) version = Column(Integer) - title = Column(String(length=255)) - publisher_id = Column(String, ForeignKey('publisher.id')) + title = Column(String(length=255), unique=True) + publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False) publisher = relationship("Publisher", back_populates="comics") current_order = Column(BIT(1)) completed = Column(BIT(1)) + issues = relationship("Issue") + story_arcs = relationship("StoryArc") + trade_paperbacks = relationship("TradePaperback") + volumes = relationship("Volume") + comic_works = relationship("ComicWork") def __repr__(self): return f'Comic({self.id} {self.version} {self.title} {self.publisher.name})' def __str__(self): return f'{self.title}({self.id})' + + +class Volume(Base): + __tablename__ = "volume" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="volumes") + issues = relationship("Issue") + + +class TradePaperback(Base): + __tablename__ = "trade_paperback" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False) + issue_start = Column(Integer) + issue_end = Column(Integer) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="trade_paperbacks") + + +class StoryArc(Base): + __tablename__ = "story_arc" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="story_arcs") + + +class Issue(Base): + __tablename__ = "issue" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + issue_number = Column(String(255)) + in_stock = Column(BIT(1)) + is_read = Column(BIT(1)) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="issues") + volume_id = Column(String, ForeignKey("volume.id"), nullable=True) + volume = relationship("Volume", back_populates="issues") + + +class Artist(Base): + __tablename__ = "artist" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False) + comic_works = relationship("ComicWork") + + +class Worktype(Base): + __tablename__ = "worktype" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False, unique=True) + comic_works = relationship("ComicWork") + + +class ComicWork(Base): + __tablename__ = "comic_work" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="comic_works") + artist_id = Column(String, ForeignKey("artist.id"), nullable=False) + artist = relationship("Artist", back_populates="comic_works") + worktype_id = Column(String, ForeignKey("worktype.id"), nullable=False) + worktype = relationship("Worktype", back_populates="comic_works") diff --git a/qt/database/metadata.py b/qt/database/metadata.py index e1b9084..46a6274 100644 --- a/qt/database/metadata.py +++ b/qt/database/metadata.py @@ -37,6 +37,7 @@ class MetaDataColumn(Base): filter_label = Column(String(255)) is_shown = Column(BIT(1)) show_filter = Column(BIT(1)) + ref_column = Column(String, nullable=True) def __repr__(self): if self.column_name is None: diff --git a/qt/database/tysc.py b/qt/database/tysc.py index e78ff17..9c94177 100644 --- a/qt/database/tysc.py +++ b/qt/database/tysc.py @@ -1,4 +1,4 @@ -from sqlalchemy import Boolean, Column, DateTime, Integer, String, ForeignKey +from sqlalchemy import Boolean, Column, DateTime, Integer, String, ForeignKey, UniqueConstraint from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import relationship @@ -7,11 +7,14 @@ from database.base import Base class Sport(Base): __tablename__ = "sport" + __table_args__ = ( + UniqueConstraint("name"), + ) id = Column(String, primary_key=True) created_date = Column(DateTime) last_modified_date = Column(DateTime) version = Column(Integer) - name = Column(String(255)) + name = Column(String(255), nullable=False, index=True, unique=True) teams = relationship("Team") positions = relationship("FieldPosition") @@ -22,47 +25,107 @@ class Team(Base): created_date = Column(DateTime) last_modified_date = Column(DateTime) version = Column(Integer) - name = Column(String(255)) - short_name = Column(String(255)) - sport_id = Column(String, ForeignKey("sport.id")) + name = Column(String(255), nullable=False, index=True, unique=True) + short_name = Column(String(255), nullable=False, ) + sport_id = Column(String, ForeignKey("sport.id"), nullable=False) sport = relationship("Sport", back_populates="positions") roosters = relationship("Rooster") class FieldPosition(Base): __tablename__ = "field_position" + __table_args__ = ( + UniqueConstraint("name", "sport_id"), + UniqueConstraint("short_name", "sport_id"), + ) id = Column(String, primary_key=True) created_date = Column(DateTime) last_modified_date = Column(DateTime) version = Column(Integer) - name = Column(String(255)) - short_name = Column(String(255)) - sport_id = Column(String, ForeignKey("sport.id")) + name = Column(String(255), nullable=False, index=True) + short_name = Column(String(255), nullable=False) + sport_id = Column(String, ForeignKey("sport.id"), nullable=False, index=True) sport = relationship("Sport", back_populates="positions") roosters = relationship("Rooster") class Player(Base): __tablename__ = "player" + __table_args__ = ( + UniqueConstraint("first_name", "last_name"), + ) id = Column(String, primary_key=True) created_date = Column(DateTime) last_modified_date = Column(DateTime) version = Column(Integer) - first_name = Column(String(255)) - last_name = Column(String(255)) + first_name = Column(String(255), nullable=False, index=True) + last_name = Column(String(255), nullable=False, index=True) roosters = relationship("Rooster") + def get_full_name(self) -> str: + return f"{self.last_name}, {self.first_name}" + class Rooster(Base): __tablename__ = "rooster" + __table_args__ = ( + UniqueConstraint("year", "team_id", "player_id", "position_id"), + ) id = Column(String, primary_key=True) created_date = Column(DateTime) last_modified_date = Column(DateTime) version = Column(Integer) year = Column(Integer) - team_id = Column(String, ForeignKey("team.id")) + team_id = Column(String, ForeignKey("team.id"), nullable=False, index=True) team = relationship("Team", back_populates="roosters") - player_id = Column(String, ForeignKey("player.id")) + player_id = Column(String, ForeignKey("player.id"), nullable=False, index=True) player = relationship("Player", back_populates="roosters") - position_id = Column(String, ForeignKey("field_position.id")) + position_id = Column(String, ForeignKey("field_position.id"), nullable=False, index=True) position = relationship("roosters") + cards = relationship("Card") + +class Vendor(Base): + __tablename__ = "vendor" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255), nullable=False, unique=True, index=True) + card_sets = relationship("CardSet") + cards = relationship("Card") + + +class CardSet(Base): + __tablename__ = "card_set" + __table_args__ = ( + UniqueConstraint("name", "vendor_id"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255), index=True) + parallel_set = Column(BIT(1)) + insert_set = Column(BIT(1)) + vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True) + vendor = relationship("Vendor", back_populates="card_sets") + cards = relationship("Card") + + +class Card(Base): + __tablename__ = "card" + __table_args__ = ( + UniqueConstraint("card_number", "year", "vendor_id", "card_set_id"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + card_number = Column(Integer, index=True) + year = Column(Integer, index=True) + card_set_id = Column(String, ForeignKey("card_set.id"), nullable=False) + card_set = relationship("cards") + rooster_id = Column(String, ForeignKey("rooster.id"), nullable=False) + rooster = relationship("cards") + vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False) + vendor = relationship("Vendor", back_populates="cards") diff --git a/qt/gui/main_window.py b/qt/gui/main_window.py index a652906..42b4383 100644 --- a/qt/gui/main_window.py +++ b/qt/gui/main_window.py @@ -68,9 +68,13 @@ class MainWindow(QMainWindow): menu_bar.addMenu(kontor_menu) kontor_menu.addAction(self.importAction) kontor_menu.addAction(self.exportAction) + comic_menu = QMenu("&Comic") + tysc_menu = QMenu("&TradeYourSportCards") media_file_menu = QMenu("&MediaFile") media_file_menu.addAction(self.updateTitleAction) media_file_menu.addAction(self.downloadAction) + kontor_menu.addMenu(comic_menu) + kontor_menu.addMenu(tysc_menu) kontor_menu.addMenu(media_file_menu) # Help menu help_menu = QMenu("&Hilfe") diff --git a/qt/gui/model_config.py b/qt/gui/model_config.py index 01d3805..c2644e9 100644 --- a/qt/gui/model_config.py +++ b/qt/gui/model_config.py @@ -43,7 +43,7 @@ class KontorModelConfig: def get_data(self) -> list: data = self.kontor_db.get_data(self._table, self.header, self.get_filter()) - print(f"KontorModelConfig.get_data: {len(data)}") + # print(f"KontorModelConfig.get_data: {len(data)}") # comics = self.kontor_db.session.query(Comic).all() # print(f'{len(comics)} Comics loaded') return data diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java index 4ac89e0..b54f74e 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java @@ -179,7 +179,7 @@ public class SetupModuleAdmin implements ApplicationListener column.getColumnName().equals(columnName))) { log.debug("Column {} with name {} of table {} found, check Values", columnOrder, columnName, table.getTableName()); MetaDataColumn column = table.getTableColumns().get(columnOrder.intValue()-1); @@ -70,6 +74,10 @@ public class MetaDataService { log.debug("filterLabel has to be change to {}}", filterLabel); column.setFilterLabel(filterLabel); } + if (refColumn != null && !refColumn.equals(column.getRefColumn())) { + log.debug("refColumn has to be change to {}}", filterLabel); + column.setRefColumn(refColumn); + } metaDataColumnRepository.save(column); } else { log.info("Column {} of table {} not found, will create it", columnName, table.getTableName()); @@ -85,6 +93,7 @@ public class MetaDataService { } } + public List findAllMetaDataColumns(String stringFilter) { if (stringFilter == null || stringFilter.isEmpty()) { log.debug("Found " + metaDataColumnRepository.count()+ " entries"); diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java index b65b099..481f237 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java @@ -29,6 +29,7 @@ public class MetaDataForm extends FormLayout { TextField columnLabel = new TextField("Column Label"); Checkbox showFilter = new Checkbox("Show Filter"); TextField filterLabel = new TextField("Filter Label"); + TextField refColumn = new TextField("Ref Column"); Button save = new com.vaadin.flow.component.button.Button("Save"); Button delete = new com.vaadin.flow.component.button.Button("Delete"); @@ -47,6 +48,7 @@ public class MetaDataForm extends FormLayout { isShown.addClickListener(click -> columnLabel.setEnabled(isShown.getValue())); add(showFilter, filterLabel); showFilter.addClickListener(click -> filterLabel.setEnabled(showFilter.getValue())); + add(refColumn); add(createButtonsLayout()); } diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java index 780126a..a5f4cca 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java @@ -66,6 +66,8 @@ public class MetaDataView extends VerticalLayout { setHeader("Zeige Filter").setWidth("6rem").setSortable(true); Grid.Column filterLabelColumn = grid.addColumn(MetaDataColumn::getFilterLabel) .setHeader("Filter Name").setResizable(true).setSortable(true); + Grid.Column refColumnColumn = grid.addColumn(MetaDataColumn::getRefColumn) + .setHeader("Ref Column Name").setResizable(true).setSortable(true); TextField searchField = new TextField(); @Getter MetaDataForm form; @@ -186,7 +188,7 @@ public class MetaDataView extends VerticalLayout { columnToggleContextMenu.addColumnToggleItem(columnLabelColumn.getHeaderText(), columnLabelColumn); columnToggleContextMenu.addColumnToggleItem(showFilterColumn.getHeaderText(), showFilterColumn); columnToggleContextMenu.addColumnToggleItem(filterLabelColumn.getHeaderText(), filterLabelColumn); - + columnToggleContextMenu.addColumnToggleItem(refColumnColumn.getHeaderText(), refColumnColumn); HorizontalLayout toolbar = new HorizontalLayout(searchField, addMetaDataButton, menuButton); toolbar.addClassName("toolbar"); return toolbar; From 820ae3d3747708842f57f27c3c3b14b764e0ebf7 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 13 Jan 2025 00:26:42 +0100 Subject: [PATCH 24/91] create files with abstract model class --- qt/database/media.py | 6 +++--- qt/database/tysc.py | 2 +- qt/gui/data_view.py | 12 ++++++++++++ qt/gui/data_view_model.py | 33 +++++++++++++++++++++++++++++++++ qt/gui/table_model.py | 15 ++++++++++----- 5 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 qt/gui/data_view.py create mode 100644 qt/gui/data_view_model.py diff --git a/qt/database/media.py b/qt/database/media.py index 621fdce..b5d793b 100644 --- a/qt/database/media.py +++ b/qt/database/media.py @@ -1,4 +1,4 @@ -from sqlalchemy import Boolean, Column, DateTime, Integer, String +from sqlalchemy import Column, DateTime, Integer, String from sqlalchemy.dialects.mysql import BIT from database.base import Base @@ -13,10 +13,10 @@ class MediaFile(Base): cloud_link = Column(String(255)) file_name = Column(String(255)) path = Column(String(255)) - review = Column(BIT(1), default=True) + review = Column(BIT(1)) title = Column(String(255)) url = Column(String(255)) - should_download = Column(Boolean, default=True) + should_download = Column(BIT(1)) def __repr__(self): return f'MediaFile({self.id} {self.title} {self.title})' diff --git a/qt/database/tysc.py b/qt/database/tysc.py index 9c94177..0e77165 100644 --- a/qt/database/tysc.py +++ b/qt/database/tysc.py @@ -1,4 +1,4 @@ -from sqlalchemy import Boolean, Column, DateTime, Integer, String, ForeignKey, UniqueConstraint +from sqlalchemy import Column, DateTime, Integer, String, ForeignKey, UniqueConstraint from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import relationship diff --git a/qt/gui/data_view.py b/qt/gui/data_view.py new file mode 100644 index 0000000..91c66d2 --- /dev/null +++ b/qt/gui/data_view.py @@ -0,0 +1,12 @@ +from abc import ABC, abstractmethod + + +class DataViewMeta(ABC): + @abstractmethod + def get_header(self): + pass + + +class ComicView(DataViewMeta): + def get_header(self): + pass diff --git a/qt/gui/data_view_model.py b/qt/gui/data_view_model.py new file mode 100644 index 0000000..e4e1bab --- /dev/null +++ b/qt/gui/data_view_model.py @@ -0,0 +1,33 @@ +from typing import List + +from PyQt5.QtCore import QAbstractTableModel +from PySide6.QtCore import QModelIndex +from PySide6.QtGui import Qt + +from gui.data_view import DataViewMeta + + +class DataViewModel(QAbstractTableModel): + def __init__(self): + super().__init__() + self.main_window = None + self._config = None + self._data = List[DataViewMeta] + + def rowCount(self, parent = QModelIndex()): + return len(self._data) + + def columnCount(self, parent = QModelIndex()): + return 0 + + def headerData(self, section, orientation, role = Qt.ItemDataRole.DisplayRole): + return None + + def data(self, index, role = Qt.ItemDataRole.DisplayRole): + return None + + def setData(self, index, value, role = Qt.ItemDataRole.EditRole): + return False + + def flags(self, index): + return None diff --git a/qt/gui/table_model.py b/qt/gui/table_model.py index 5e80c3b..e11dacc 100644 --- a/qt/gui/table_model.py +++ b/qt/gui/table_model.py @@ -45,8 +45,8 @@ class KontorTableModel(QAbstractTableModel): if self._data is None: return None value = self._data[index.row()][index.column()] - if role == Qt.ItemDataRole.DisplayRole: - # print('{}: {}'.format(value, type(value))) + # print('{}:: {}:: {}: {}'.format(index, role, value, type(value))) + if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole: if isinstance(value, datetime): return value.strftime("%Y-%m-%d %M:%M:%S") if isinstance(value, str): @@ -57,6 +57,7 @@ class KontorTableModel(QAbstractTableModel): else: return self._main_window.cross if isinstance(value, int): + print('{}:: {}: {}'.format(index, value, type(value))) if value == 1: return self._main_window.tick else: @@ -69,7 +70,6 @@ class KontorTableModel(QAbstractTableModel): return str(value) if role == Qt.ItemDataRole.DecorationRole: if isinstance(value, bytes): - # print('{}: {}'.format(value, type(value))) if value == b'\x01': return self._main_window.tick else: @@ -91,13 +91,18 @@ class KontorTableModel(QAbstractTableModel): # print(f"Header count: {len(self._config.get_header())}") return len(self._config.header) - def setData(self, index, value, role=Qt.ItemDataRole.EditRole): + def setData(self, index, value, role: int) -> bool: + print(index, role) if role == Qt.ItemDataRole.EditRole: self._data[index.row()][index.column()] = value + print(self._data[index.row()][index.column()]) + self.dataChanged.emit(index, index) + return True if role == Qt.ItemDataRole.CheckStateRole: + print("role == Qt.ItemDataRole.CheckStateRole") checked = value == Qt.CheckState.Checked self._data[index.row()][index.column()] = checked - return True + return False def flags(self, index): return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate From d0eae1980a2af1846d346f5d20b2bd3ce4243530 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 13 Jan 2025 16:18:13 +0100 Subject: [PATCH 25/91] add export to json --- qt/database/__init__.py | 118 +++++++++++++++++++++++++++++++------- qt/database/base.py | 19 +++++- qt/database/comic.py | 8 +-- qt/gui/data_view_model.py | 13 ++--- qt/gui/dialogs.py | 4 +- qt/gui/main_window.py | 11 ++-- qt/gui/model_config.py | 31 +++++----- qt/gui/table_model.py | 2 +- 8 files changed, 152 insertions(+), 54 deletions(-) diff --git a/qt/database/__init__.py b/qt/database/__init__.py index c0191cc..d74e927 100644 --- a/qt/database/__init__.py +++ b/qt/database/__init__.py @@ -1,3 +1,7 @@ +import json +from datetime import datetime +from pathlib import Path + import mariadb from sqlalchemy import create_engine, select, text, MetaData, join from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker @@ -30,35 +34,67 @@ class KontorDB: __session__ = sessionmaker(bind=engine) self.session = __session__() - def get_table_id(self, table_name): - result = self.session.execute(select(MetaDataTable.id).where(MetaDataTable.table_name == table_name)).scalar() - return result - def get_table_names(self) -> list: tables = self.session.query(MetaDataTable).all() result = [table.table_name for table in tables] return result - def get_column_meta_data(self, table_id: str, table_name: str) -> dict: + def get_column_meta_data(self, table_name: str, view_only=True) -> dict: meta_data = {} order = 0 - for (_, column) in self.session.query(MetaDataTable, MetaDataColumn).filter(MetaDataTable.id == MetaDataColumn.table_id).filter(MetaDataTable.table_name == table_name).filter(MetaDataColumn.is_shown == 1).all(): - meta_data[order] = {'column': column.column_name, 'label': column.column_label, 'order': column.column_order, 'ref_column': column.ref_column} - order += 1 + if view_only: + for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name). + filter(MetaDataColumn.is_shown == 1).all()): + meta_data[order] = {'column': column.column_name, 'label': column.column_label, + 'order': column.column_order, 'ref_column': column.ref_column} + order += 1 + else: + for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name).all()): + meta_data[order] = { + 'column': column.column_name, + 'order': column.column_order, + 'ref_column': column.ref_column + } + order += 1 return meta_data - def get_filters(self, table_id): - cursor = self.db_conn.cursor() - filters = {} - cursor.execute( - "SELECT column_name, filter_label from meta_data_column WHERE table_id=? AND show_filter is true", - (table_id,)) - rows = cursor.fetchall() - for row in rows: - filters[row[0]] = {'label': row[1], 'widget': None} - cursor.close() - # print(f"retrieved {len(rows)} filters: {filters}") - return filters + def get_filters(self, table_name): + _filter_map = {} + for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name). + filter(MetaDataColumn.show_filter == 1).all()): + _filter_map[column.column_name] = {'label': column.filter_label, 'widget': None} + print(f"retrieved {len(_filter_map)} filters: {_filter_map}") + return _filter_map + + def data(self, table, columns: dict, filters) -> list: + data = [] + entries = [] + if len(filters) == 0: + entries = self.session.query(table).all() + else: + entries = self.session.query(table).filter_by(**filters) + for entry in entries: + row = [] + for order in columns.keys(): + column_name = columns[order]['column'] + if str(column_name).endswith("_id"): + ref_table = column_name[:-3] + # print(f"{ref_table=}") + ref = getattr(entry, ref_table) + value = getattr(ref, "name") + # print(f"{value=}") + row.append(value) + else: + row.append(getattr(entry, column_name)) + # print(repr(row)) + data.append(row) + return data def get_data(self, table_name: str, columns: dict, where_clause: str) -> list: data = [] @@ -103,3 +139,45 @@ class KontorDB: statement = f"SELECT {columns} FROM {table} {where_clause}" print(f"{statement=}") return statement + + def export_db(self, export_type: str, export_file_name: str, export_table_list: list): + print(f"export DB to {export_file_name} as {export_type}") + db = {} + for table in export_table_list: + columns = self.get_column_meta_data(table, view_only=False) + model = Base.model_lookup_by_table_name(table) + rows = self.session.query(model).all() + entries = [] + print(f"found {len(rows)} entries") + print(f"found {len(columns)} columns") + for row in rows: + print(row) + entry = {} + for order in columns: + print(columns[order]) + column_name = columns[order]['column'] + print(f"get value {column_name} from {row} of table {table}") + try: + value = getattr(row, column_name) + if isinstance(value, datetime): + entry[column_name] = str(value) + else: + entry[column_name] = value + except AttributeError as error: + print("could not get value") + entries.append(entry) + db[table] = entries + export_file = Path(export_file_name) + match export_type: + case "JSON": + json_dump = json.dumps(db, indent=4) + with open(export_file_name, "w") as dump_file: + dump_file.write(json_dump) + case "YAML": + export_file = Path(export_file_name) + case "SQLite": + export_file = Path(export_file_name) + case _: + print("unknown export type") + if export_file.exists(): + print(f"{export_file} exists") diff --git a/qt/database/base.py b/qt/database/base.py index 9339e51..f97d724 100644 --- a/qt/database/base.py +++ b/qt/database/base.py @@ -1,7 +1,20 @@ from sqlalchemy import Column, String, DateTime, Integer -from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker +from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker, declarative_base -class Base(DeclarativeBase): - pass +# class Base(DeclarativeBase): +# pass +class BaseModel: + + @classmethod + def model_lookup_by_table_name(cls, table_name): + registry_instance = getattr(cls, "registry") + for mapper_ in registry_instance.mappers: + model = mapper_.class_ + model_class_name = model.__tablename__ + if model_class_name == table_name: + return model + + +Base = declarative_base(cls=BaseModel) \ No newline at end of file diff --git a/qt/database/comic.py b/qt/database/comic.py index dc4204f..1dc31b4 100644 --- a/qt/database/comic.py +++ b/qt/database/comic.py @@ -1,4 +1,4 @@ -from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import relationship @@ -106,7 +106,7 @@ class Artist(Base): comic_works = relationship("ComicWork") -class Worktype(Base): +class WorkType(Base): __tablename__ = "worktype" id = Column(String, primary_key=True) created_date = Column(DateTime) @@ -126,5 +126,5 @@ class ComicWork(Base): comic = relationship("Comic", back_populates="comic_works") artist_id = Column(String, ForeignKey("artist.id"), nullable=False) artist = relationship("Artist", back_populates="comic_works") - worktype_id = Column(String, ForeignKey("worktype.id"), nullable=False) - worktype = relationship("Worktype", back_populates="comic_works") + work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False) + work_type = relationship("WorkType", back_populates="comic_works") diff --git a/qt/gui/data_view_model.py b/qt/gui/data_view_model.py index e4e1bab..4ffdc8c 100644 --- a/qt/gui/data_view_model.py +++ b/qt/gui/data_view_model.py @@ -1,7 +1,6 @@ from typing import List -from PyQt5.QtCore import QAbstractTableModel -from PySide6.QtCore import QModelIndex +from PySide6.QtCore import QModelIndex, QAbstractTableModel from PySide6.QtGui import Qt from gui.data_view import DataViewMeta @@ -14,19 +13,19 @@ class DataViewModel(QAbstractTableModel): self._config = None self._data = List[DataViewMeta] - def rowCount(self, parent = QModelIndex()): + def rowCount(self, parent=QModelIndex()): return len(self._data) - def columnCount(self, parent = QModelIndex()): + def columnCount(self, parent=QModelIndex()): return 0 - def headerData(self, section, orientation, role = Qt.ItemDataRole.DisplayRole): + def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole): return None - def data(self, index, role = Qt.ItemDataRole.DisplayRole): + def data(self, index, role=Qt.ItemDataRole.DisplayRole): return None - def setData(self, index, value, role = Qt.ItemDataRole.EditRole): + def setData(self, index, value, role=Qt.ItemDataRole.EditRole): return False def flags(self, index): diff --git a/qt/gui/dialogs.py b/qt/gui/dialogs.py index 703baeb..21dbe92 100644 --- a/qt/gui/dialogs.py +++ b/qt/gui/dialogs.py @@ -1,7 +1,7 @@ from pathlib import Path from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QFileDialog, \ - QGroupBox, QCheckBox, QComboBox + QCheckBox, QComboBox class ExportKontorDialog(QDialog): @@ -10,7 +10,7 @@ class ExportKontorDialog(QDialog): self.parent = parent self.kontor_db = kontor_db - self.file_name = None + self.file_name = "data.json" self.tables = [] self._table_options = {} diff --git a/qt/gui/main_window.py b/qt/gui/main_window.py index 42b4383..f47abd5 100644 --- a/qt/gui/main_window.py +++ b/qt/gui/main_window.py @@ -3,6 +3,8 @@ from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidg from PySide6.QtWidgets import QLabel, QMainWindow from database import KontorDB +from database.media import MediaFile +from database.comic import Comic from gui.dialogs import ExportKontorDialog, ImportKontorDialog from gui.model_config import KontorModelConfig from gui.table_model import KontorTableModel @@ -33,8 +35,8 @@ class MainWindow(QMainWindow): parent_layout = QVBoxLayout() self.central_widget.setLayout(parent_layout) self.tabs = QTabWidget() - self.tabs.addTab(self.generate_data_tab("comic"), "Comics") - self.tabs.addTab(self.generate_data_tab("media_file"), "MediaFile") + self.tabs.addTab(self.generate_data_tab("comic", Comic), "Comics") + self.tabs.addTab(self.generate_data_tab("media_file", MediaFile), "MediaFile") self.tabs.currentChanged.connect(self._tab_changed) #label.setAlignment(Qt.AlignmentFlag.AlignCenter) parent_layout.addWidget(self.tabs) @@ -111,6 +113,7 @@ class MainWindow(QMainWindow): print(export_dlg.get_tables_to_export()) print(f"export DB to {export_dlg.file_name}") self.statusBar.showMessage(f"export DB to {export_dlg.file_name}", 3000) + self.kontor_db.export_db(export_dlg.current_export_type, export_dlg.file_name, export_dlg.get_tables_to_export()) else: self.statusBar.showMessage("Export cancelled", 3000) @@ -120,9 +123,9 @@ class MainWindow(QMainWindow): def _tab_changed(self, tab_index): self.data[tab_index].refresh() - def generate_data_tab(self, table_name): + def generate_data_tab(self, table_name, table): data_tab = QWidget() - table_config = KontorModelConfig(self.kontor_db, self, table_name) + table_config = KontorModelConfig(self.kontor_db, self, table_name, table) model = KontorTableModel(table_config) layout = QVBoxLayout() self.data.append(model) diff --git a/qt/gui/model_config.py b/qt/gui/model_config.py index c2644e9..a3f902d 100644 --- a/qt/gui/model_config.py +++ b/qt/gui/model_config.py @@ -6,25 +6,18 @@ from database import KontorDB class KontorModelConfig: - def __init__(self, kontor_db: KontorDB, main_window, table_name: str): + def __init__(self, kontor_db: KontorDB, main_window, table_name: str, table): self.header = {} self.filter = {} self.main_window = main_window - self._table = table_name - self._table_id = None + self._table_name = table_name + self._table = table self.kontor_db = kontor_db self.get_table_config() - def get_table_id(self): - if self._table_id is not None: - return - self._table_id = self.kontor_db.get_table_id(self._table) - def get_table_config(self): - if self._table_id is None: - self.get_table_id() - self.header = self.kontor_db.get_column_meta_data(self._table_id, self._table) - self.filter = self.kontor_db.get_filters(self._table_id) + self.header = self.kontor_db.get_column_meta_data(self._table_name) + self.filter = self.kontor_db.get_filters(self._table_name) def get_filter(self) -> str: filter_rule = "" @@ -41,8 +34,20 @@ class KontorModelConfig: # print(f"{filter_rule=}") return filter_rule + def filters(self) -> dict: + _filters = {} + # print(self.filter["download"].isChecked()) + for column, filter_info in self.filter.items(): + # print(column, filter_info) + if filter_info['widget'].isChecked(): + _filters[column] = True + # print(f"{filter_rule=}") + return _filters + def get_data(self) -> list: - data = self.kontor_db.get_data(self._table, self.header, self.get_filter()) + # data = self.kontor_db.get_data(self._table_name, self.header, self.get_filter()) + # data.clear() + data = self.kontor_db.data(self._table, self.header, self.filters()) # print(f"KontorModelConfig.get_data: {len(data)}") # comics = self.kontor_db.session.query(Comic).all() # print(f'{len(comics)} Comics loaded') diff --git a/qt/gui/table_model.py b/qt/gui/table_model.py index e11dacc..e1ba6ae 100644 --- a/qt/gui/table_model.py +++ b/qt/gui/table_model.py @@ -57,7 +57,7 @@ class KontorTableModel(QAbstractTableModel): else: return self._main_window.cross if isinstance(value, int): - print('{}:: {}: {}'.format(index, value, type(value))) + # print('{}:: {}: {}'.format(index, value, type(value))) if value == 1: return self._main_window.tick else: From f74c07af9aec29840fb9a635cd43da710f78f02c Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 13 Jan 2025 22:54:25 +0100 Subject: [PATCH 26/91] add cli app and fix relationship typos --- python/cli/.gitignore | 105 +++++++++ python/cli/CHANGELOG.md | 5 + python/cli/Dockerfile | 10 + python/cli/LICENSE.md | 1 + python/cli/MANIFEST.in | 5 + python/cli/Makefile | 31 +++ python/cli/README.md | 69 ++++++ python/cli/config/kontor.yml.example | 46 ++++ .../__init__.py => python/cli/docs/.gitkeep | 0 python/cli/kontor/__init__.py | 0 python/cli/kontor/controllers/__init__.py | 0 python/cli/kontor/controllers/clibase.py | 60 +++++ python/cli/kontor/core/__init__.py | 0 python/cli/kontor/core/exc.py | 4 + python/cli/kontor/core/version.py | 7 + .../cli/kontor}/database/__init__.py | 6 +- {qt => python/cli/kontor}/database/base.py | 0 {qt => python/cli/kontor}/database/comic.py | 2 +- python/cli/kontor/database/media.py | 25 +++ python/cli/kontor/database/metadata.py | 49 ++++ {qt => python/cli/kontor}/database/tysc.py | 2 +- python/cli/kontor/ext/__init__.py | 0 python/cli/kontor/main.py | 111 +++++++++ python/cli/kontor/plugins/__init__.py | 0 python/cli/kontor/templates/__init__.py | 0 python/cli/kontor/templates/command1.jinja2 | 4 + python/cli/requirements-dev.txt | 8 + python/cli/requirements.txt | 6 + python/cli/setup.cfg | 0 python/cli/setup.py | 28 +++ python/cli/tests/conftest.py | 16 ++ python/cli/tests/test_kontor.py | 36 +++ python/main.py | 16 ++ {qt => python/qt}/.gitignore | 0 python/qt/database/__init__.py | 212 ++++++++++++++++++ python/qt/database/base.py | 18 ++ python/qt/database/comic.py | 130 +++++++++++ {qt => python/qt}/database/media.py | 0 {qt => python/qt}/database/metadata.py | 0 python/qt/database/tysc.py | 131 +++++++++++ python/qt/gui/__init__.py | 0 {qt => python/qt}/gui/data_view.py | 0 {qt => python/qt}/gui/data_view_model.py | 0 {qt => python/qt}/gui/dialogs.py | 0 {qt => python/qt}/gui/main_window.py | 0 {qt => python/qt}/gui/model_config.py | 0 {qt => python/qt}/gui/table_model.py | 0 {qt => python/qt}/kontor.py | 0 {qt => python/qt}/pysidedeploy.spec | 0 {qt => python/qt}/res/application-export.png | Bin {qt => python/qt}/res/application-import.png | Bin {qt => python/qt}/res/arrow-circle-double.png | Bin {qt => python/qt}/res/cross.png | Bin {qt => python/qt}/res/tick.png | Bin 54 files changed, 1138 insertions(+), 5 deletions(-) create mode 100644 python/cli/.gitignore create mode 100644 python/cli/CHANGELOG.md create mode 100644 python/cli/Dockerfile create mode 100644 python/cli/LICENSE.md create mode 100644 python/cli/MANIFEST.in create mode 100644 python/cli/Makefile create mode 100644 python/cli/README.md create mode 100644 python/cli/config/kontor.yml.example rename qt/gui/__init__.py => python/cli/docs/.gitkeep (100%) create mode 100644 python/cli/kontor/__init__.py create mode 100644 python/cli/kontor/controllers/__init__.py create mode 100644 python/cli/kontor/controllers/clibase.py create mode 100644 python/cli/kontor/core/__init__.py create mode 100644 python/cli/kontor/core/exc.py create mode 100644 python/cli/kontor/core/version.py rename {qt => python/cli/kontor}/database/__init__.py (98%) rename {qt => python/cli/kontor}/database/base.py (100%) rename {qt => python/cli/kontor}/database/comic.py (99%) create mode 100644 python/cli/kontor/database/media.py create mode 100644 python/cli/kontor/database/metadata.py rename {qt => python/cli/kontor}/database/tysc.py (99%) create mode 100644 python/cli/kontor/ext/__init__.py create mode 100644 python/cli/kontor/main.py create mode 100644 python/cli/kontor/plugins/__init__.py create mode 100644 python/cli/kontor/templates/__init__.py create mode 100644 python/cli/kontor/templates/command1.jinja2 create mode 100644 python/cli/requirements-dev.txt create mode 100644 python/cli/requirements.txt create mode 100644 python/cli/setup.cfg create mode 100644 python/cli/setup.py create mode 100644 python/cli/tests/conftest.py create mode 100644 python/cli/tests/test_kontor.py create mode 100644 python/main.py rename {qt => python/qt}/.gitignore (100%) create mode 100644 python/qt/database/__init__.py create mode 100644 python/qt/database/base.py create mode 100644 python/qt/database/comic.py rename {qt => python/qt}/database/media.py (100%) rename {qt => python/qt}/database/metadata.py (100%) create mode 100644 python/qt/database/tysc.py create mode 100644 python/qt/gui/__init__.py rename {qt => python/qt}/gui/data_view.py (100%) rename {qt => python/qt}/gui/data_view_model.py (100%) rename {qt => python/qt}/gui/dialogs.py (100%) rename {qt => python/qt}/gui/main_window.py (100%) rename {qt => python/qt}/gui/model_config.py (100%) rename {qt => python/qt}/gui/table_model.py (100%) rename {qt => python/qt}/kontor.py (100%) rename {qt => python/qt}/pysidedeploy.spec (100%) rename {qt => python/qt}/res/application-export.png (100%) rename {qt => python/qt}/res/application-import.png (100%) rename {qt => python/qt}/res/arrow-circle-double.png (100%) rename {qt => python/qt}/res/cross.png (100%) rename {qt => python/qt}/res/tick.png (100%) diff --git a/python/cli/.gitignore b/python/cli/.gitignore new file mode 100644 index 0000000..a74b246 --- /dev/null +++ b/python/cli/.gitignore @@ -0,0 +1,105 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +coverage-report/ +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/python/cli/CHANGELOG.md b/python/cli/CHANGELOG.md new file mode 100644 index 0000000..6c95d97 --- /dev/null +++ b/python/cli/CHANGELOG.md @@ -0,0 +1,5 @@ +# Kontor Change History + +## 0.0.1 + +Initial release. diff --git a/python/cli/Dockerfile b/python/cli/Dockerfile new file mode 100644 index 0000000..d32cf5c --- /dev/null +++ b/python/cli/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.9-alpine +LABEL MAINTAINER="Thomas Peetz " +ENV PS1="\[\e[0;33m\]|> kontor <| \[\e[1;35m\]\W\[\e[0m\] \[\e[0m\]# " + +WORKDIR /src +COPY . /src +RUN pip install --no-cache-dir -r requirements.txt \ + && python setup.py install +WORKDIR / +ENTRYPOINT ["kontor"] diff --git a/python/cli/LICENSE.md b/python/cli/LICENSE.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/python/cli/LICENSE.md @@ -0,0 +1 @@ + diff --git a/python/cli/MANIFEST.in b/python/cli/MANIFEST.in new file mode 100644 index 0000000..1160952 --- /dev/null +++ b/python/cli/MANIFEST.in @@ -0,0 +1,5 @@ +recursive-include *.py +include setup.cfg +include README.md CHANGELOG.md LICENSE.md +include *.txt +recursive-include kontor/templates * diff --git a/python/cli/Makefile b/python/cli/Makefile new file mode 100644 index 0000000..b016c3c --- /dev/null +++ b/python/cli/Makefile @@ -0,0 +1,31 @@ +.PHONY: clean virtualenv test docker dist dist-upload + +clean: + find . -name '*.py[co]' -delete + +virtualenv: + virtualenv --prompt '|> kontor <| ' env + env/bin/pip install -r requirements-dev.txt + env/bin/python setup.py develop + @echo + @echo "VirtualENV Setup Complete. Now run: source env/bin/activate" + @echo + +test: + python -m pytest \ + -v \ + --cov=kontor \ + --cov-report=term \ + --cov-report=html:coverage-report \ + tests/ + +docker: clean + docker build -t kontor:latest . + +dist: clean + rm -rf dist/* + python setup.py sdist + python setup.py bdist_wheel + +dist-upload: + twine upload dist/* diff --git a/python/cli/README.md b/python/cli/README.md new file mode 100644 index 0000000..6afe593 --- /dev/null +++ b/python/cli/README.md @@ -0,0 +1,69 @@ +# Kontor CLI Tool + +## Installation + +``` +$ pip install -r requirements.txt + +$ python setup.py install +``` + +## Development + +This project includes a number of helpers in the `Makefile` to streamline common development tasks. + +### Environment Setup + +The following demonstrates setting up and working with a development environment: + +``` +### create a virtualenv for development + +$ make virtualenv + +$ source env/bin/activate + + +### run kontor cli application + +$ kontor --help + + +### run pytest / coverage + +$ make test +``` + + +### Releasing to PyPi + +Before releasing to PyPi, you must configure your login credentials: + +**~/.pypirc**: + +``` +[pypi] +username = YOUR_USERNAME +password = YOUR_PASSWORD +``` + +Then use the included helper function via the `Makefile`: + +``` +$ make dist + +$ make dist-upload +``` + +## Deployments + +### Docker + +Included is a basic `Dockerfile` for building and distributing `Kontor`, +and can be built with the included `make` helper: + +``` +$ make docker + +$ docker run -it kontor --help +``` diff --git a/python/cli/config/kontor.yml.example b/python/cli/config/kontor.yml.example new file mode 100644 index 0000000..ba6507c --- /dev/null +++ b/python/cli/config/kontor.yml.example @@ -0,0 +1,46 @@ +### Kontor Configuration Settings +--- + +kontor: + +### Toggle application level debug (does not toggle framework debugging) +# debug: false + +### Where external (third-party) plugins are loaded from +# plugin_dir: /var/lib/kontor/plugins/ + +### Where all plugin configurations are loaded from +# plugin_config_dir: /etc/kontor/plugins.d/ + +### Where external templates are loaded from +# template_dir: /var/lib/kontor/templates/ + +### The log handler label +# log_handler: colorlog + +### The output handler label +# output_handler: jinja2 + +### sample foo option +# foo: bar + + +log.colorlog: + +### Where the log file lives (no log file by default) +# file: null + +### The level for which to log. One of: info, warning, error, fatal, debug +# level: info + +### Whether or not to log to console +# to_console: true + +### Whether or not to rotate the log file when it reaches `max_bytes` +# rotate: false + +### Max size in bytes that a log file can grow until it is rotated. +# max_bytes: 512000 + +### The maximum number of log files to maintain when rotating +# max_files: 4 diff --git a/qt/gui/__init__.py b/python/cli/docs/.gitkeep similarity index 100% rename from qt/gui/__init__.py rename to python/cli/docs/.gitkeep diff --git a/python/cli/kontor/__init__.py b/python/cli/kontor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/kontor/controllers/__init__.py b/python/cli/kontor/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/kontor/controllers/clibase.py b/python/cli/kontor/controllers/clibase.py new file mode 100644 index 0000000..8d69fb2 --- /dev/null +++ b/python/cli/kontor/controllers/clibase.py @@ -0,0 +1,60 @@ + +from cement import Controller, ex +from cement.utils.version import get_version_banner +from ..core.version import get_version + +VERSION_BANNER = """ +Kontor CLI Tool %s +%s +""" % (get_version(), get_version_banner()) + + +class CliBase(Controller): + class Meta: + label = 'base' + + # text displayed at the top of --help output + description = 'Kontor CLI Tool' + + # text displayed at the bottom of --help output + epilog = 'Usage: kontor command1 --foo bar' + + # controller level arguments. ex: 'kontor --version' + arguments = [ + ### add a version banner + ( [ '-v', '--version' ], + { 'action' : 'version', + 'version' : VERSION_BANNER } ), + ] + + + def _default(self): + """Default action if no sub-command is passed.""" + + self.app.args.print_help() + + + @ex( + help='example sub command1', + + # sub-command level arguments. ex: 'kontor command1 --foo bar' + arguments=[ + ### add a sample foo option under subcommand namespace + ( [ '-f', '--foo' ], + { 'help' : 'notorious foo option', + 'action' : 'store', + 'dest' : 'foo' } ), + ], + ) + def command1(self): + """Example sub-command.""" + + data = { + 'foo' : 'bar', + } + + ### do something with arguments + if self.app.pargs.foo is not None: + data['foo'] = self.app.pargs.foo + + self.app.render(data, 'command1.jinja2') diff --git a/python/cli/kontor/core/__init__.py b/python/cli/kontor/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/kontor/core/exc.py b/python/cli/kontor/core/exc.py new file mode 100644 index 0000000..aaeb159 --- /dev/null +++ b/python/cli/kontor/core/exc.py @@ -0,0 +1,4 @@ + +class KontorError(Exception): + """Generic errors.""" + pass diff --git a/python/cli/kontor/core/version.py b/python/cli/kontor/core/version.py new file mode 100644 index 0000000..d130f85 --- /dev/null +++ b/python/cli/kontor/core/version.py @@ -0,0 +1,7 @@ + +from cement.utils.version import get_version as cement_get_version + +VERSION = (0, 0, 1, 'alpha', 0) + +def get_version(version=VERSION): + return cement_get_version(version) diff --git a/qt/database/__init__.py b/python/cli/kontor/database/__init__.py similarity index 98% rename from qt/database/__init__.py rename to python/cli/kontor/database/__init__.py index d74e927..95ce1d8 100644 --- a/qt/database/__init__.py +++ b/python/cli/kontor/database/__init__.py @@ -6,9 +6,9 @@ import mariadb from sqlalchemy import create_engine, select, text, MetaData, join from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker -from database.base import Base -from database.comic import Comic -from database.metadata import MetaDataTable, MetaDataColumn +from .base import Base +from .comic import Comic +from .metadata import MetaDataTable, MetaDataColumn class KontorDB: diff --git a/qt/database/base.py b/python/cli/kontor/database/base.py similarity index 100% rename from qt/database/base.py rename to python/cli/kontor/database/base.py diff --git a/qt/database/comic.py b/python/cli/kontor/database/comic.py similarity index 99% rename from qt/database/comic.py rename to python/cli/kontor/database/comic.py index 1dc31b4..49d222f 100644 --- a/qt/database/comic.py +++ b/python/cli/kontor/database/comic.py @@ -2,7 +2,7 @@ from sqlalchemy import Column, DateTime, ForeignKey, Integer, String from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import relationship -from database.base import Base +from .base import Base class Publisher(Base): diff --git a/python/cli/kontor/database/media.py b/python/cli/kontor/database/media.py new file mode 100644 index 0000000..0129c03 --- /dev/null +++ b/python/cli/kontor/database/media.py @@ -0,0 +1,25 @@ +from sqlalchemy import Column, DateTime, Integer, String +from sqlalchemy.dialects.mysql import BIT + +from .base import Base + + +class MediaFile(Base): + __tablename__ = 'media_file' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + cloud_link = Column(String(255)) + file_name = Column(String(255)) + path = Column(String(255)) + review = Column(BIT(1)) + title = Column(String(255)) + url = Column(String(255)) + should_download = Column(BIT(1)) + + def __repr__(self): + return f'MediaFile({self.id} {self.title} {self.title})' + + def __str__(self): + return f'{self.title}({self.id})' diff --git a/python/cli/kontor/database/metadata.py b/python/cli/kontor/database/metadata.py new file mode 100644 index 0000000..2975e10 --- /dev/null +++ b/python/cli/kontor/database/metadata.py @@ -0,0 +1,49 @@ +from sqlalchemy import Column, String, ForeignKey, DateTime, Integer, Boolean +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from .base import Base + + +class MetaDataTable(Base): + __tablename__ = 'meta_data_table' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + table_name = Column(String(255), unique=True) + table_columns = relationship("MetaDataColumn") + + def __repr__(self): + return f'MetaDataTable({self.id} {self.table_name})' + + def __str__(self): + return f'{self.table_name}({self.id})' + +class MetaDataColumn(Base): + __tablename__ = 'meta_data_column' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + column_modifier = Column(String(255), nullable=True) + column_name = Column(String(255)) + column_order = Column(Integer) + column_sync_name = Column(String(255)) + column_type = Column(String(255)) + table_id = Column(String, ForeignKey('meta_data_table.id')) + table = relationship("MetaDataTable", back_populates="table_columns") + column_label = Column(String(255)) + filter_label = Column(String(255)) + is_shown = Column(BIT(1)) + show_filter = Column(BIT(1)) + ref_column = Column(String, nullable=True) + + def __repr__(self): + if self.column_name is None: + return f'MetaDataColumn({self.id} {self.table.table_name}.__)' + else: + return f'MetaDataColumn({self.id} {self.table.table_name}.{self.column_name})' + + def __str__(self): + return f'{self.column_name}({self.id})' diff --git a/qt/database/tysc.py b/python/cli/kontor/database/tysc.py similarity index 99% rename from qt/database/tysc.py rename to python/cli/kontor/database/tysc.py index 0e77165..54cacea 100644 --- a/qt/database/tysc.py +++ b/python/cli/kontor/database/tysc.py @@ -2,7 +2,7 @@ from sqlalchemy import Column, DateTime, Integer, String, ForeignKey, UniqueCons from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import relationship -from database.base import Base +from .base import Base class Sport(Base): diff --git a/python/cli/kontor/ext/__init__.py b/python/cli/kontor/ext/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/kontor/main.py b/python/cli/kontor/main.py new file mode 100644 index 0000000..4c71aab --- /dev/null +++ b/python/cli/kontor/main.py @@ -0,0 +1,111 @@ + +from cement import App, TestApp, init_defaults +from cement.core.exc import CaughtSignal +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from .core.exc import KontorError +from .database.base import Base +from .controllers.clibase import CliBase + +# configuration defaults +CONFIG = init_defaults('kontor', 'mariadb') +CONFIG['kontor']['foo'] = 'bar' +CONFIG['mariadb']['user'] = 'kontor' +CONFIG['mariadb']['password'] = 'kontor' +CONFIG['mariadb']['host'] = '127.0.0.1' +CONFIG['mariadb']['port'] = '3306' +CONFIG['mariadb']['database'] = 'kontor' + +def extend_sqlalchemy(app): + app.log.info('extending kontor application with sqlalchemy') + connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format( + app.config.get('mariadb', 'user'), + app.config.get('mariadb', 'password'), + app.config.get('mariadb', 'host'), + app.config.get('mariadb', 'port'), + app.config.get('mariadb', 'database') + )) + # engine = create_engine(connect_string, echo=True) + engine = create_engine(connect_string) + Base.metadata.create_all(bind=engine) + __session__ = sessionmaker(bind=engine) + app.extend('session', __session__()) + + +class Kontor(App): + """Kontor primary application.""" + + class Meta: + label = 'kontor' + + # configuration defaults + config_defaults = CONFIG + + # call sys.exit() on close + exit_on_close = True + + # load additional framework extensions + extensions = [ + 'yaml', + 'colorlog', + 'jinja2', + ] + + # configuration handler + config_handler = 'yaml' + + # configuration file suffix + config_file_suffix = '.yml' + + # set the log handler + log_handler = 'colorlog' + + # set the output handler + output_handler = 'jinja2' + + hooks = [ + ('post_setup', extend_sqlalchemy), + ] + # register handlers + handlers = [ + CliBase + ] + + +class KontorTest(TestApp,Kontor): + """A sub-class of Kontor that is better suited for testing.""" + + class Meta: + label = 'kontor' + + +def main(): + with Kontor() as app: + try: + app.run() + + except AssertionError as e: + print('AssertionError > %s' % e.args[0]) + app.exit_code = 1 + + if app.debug is True: + import traceback + traceback.print_exc() + + except KontorError as e: + print('KontorError > %s' % e.args[0]) + app.exit_code = 1 + + if app.debug is True: + import traceback + traceback.print_exc() + + except CaughtSignal as e: + # Default Cement signals are SIGINT and SIGTERM, exit 0 (non-error) + print('\n%s' % e) + app.exit_code = 0 + + +if __name__ == '__main__': + main() diff --git a/python/cli/kontor/plugins/__init__.py b/python/cli/kontor/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/kontor/templates/__init__.py b/python/cli/kontor/templates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/kontor/templates/command1.jinja2 b/python/cli/kontor/templates/command1.jinja2 new file mode 100644 index 0000000..2435e4d --- /dev/null +++ b/python/cli/kontor/templates/command1.jinja2 @@ -0,0 +1,4 @@ + +Example Template (templates/command1.jinja2) + +Foo => {{ foo }} diff --git a/python/cli/requirements-dev.txt b/python/cli/requirements-dev.txt new file mode 100644 index 0000000..f20606e --- /dev/null +++ b/python/cli/requirements-dev.txt @@ -0,0 +1,8 @@ +-r requirements.txt + +pytest +pytest-cov +coverage +twine>=1.11.0 +setuptools>=38.6.0 +wheel>=0.31.0 diff --git a/python/cli/requirements.txt b/python/cli/requirements.txt new file mode 100644 index 0000000..bee2df1 --- /dev/null +++ b/python/cli/requirements.txt @@ -0,0 +1,6 @@ +cement==3.0.12 +cement[jinja2] +cement[yaml] +cement[colorlog] +mariadb +sqlalchemy diff --git a/python/cli/setup.cfg b/python/cli/setup.cfg new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/setup.py b/python/cli/setup.py new file mode 100644 index 0000000..0b92a43 --- /dev/null +++ b/python/cli/setup.py @@ -0,0 +1,28 @@ + +from setuptools import setup, find_packages +from kontor.core.version import get_version + +VERSION = get_version() + +f = open('README.md', 'r') +LONG_DESCRIPTION = f.read() +f.close() + +setup( + name='kontor', + version=VERSION, + description='Kontor CLI Tool', + long_description=LONG_DESCRIPTION, + long_description_content_type='text/markdown', + author='Thomas Peetz', + author_email='thomas.peetz@thpeetz.de', + url='https://gitlab.com/tpeetz/kontor', + license='MIT', + packages=find_packages(exclude=['ez_setup', 'tests*']), + package_data={'kontor': ['templates/*']}, + include_package_data=True, + entry_points=""" + [console_scripts] + kontor = kontor.main:main + """, +) diff --git a/python/cli/tests/conftest.py b/python/cli/tests/conftest.py new file mode 100644 index 0000000..5124e2e --- /dev/null +++ b/python/cli/tests/conftest.py @@ -0,0 +1,16 @@ +""" +PyTest Fixtures. +""" + +import pytest +from cement import fs + +@pytest.fixture(scope="function") +def tmp(request): + """ + Create a `tmp` object that geneates a unique temporary directory, and file + for each test function that requires it. + """ + t = fs.Tmp() + yield t + t.remove() diff --git a/python/cli/tests/test_kontor.py b/python/cli/tests/test_kontor.py new file mode 100644 index 0000000..3c1bd67 --- /dev/null +++ b/python/cli/tests/test_kontor.py @@ -0,0 +1,36 @@ + +from pytest import raises +from kontor.main import KontorTest + +def test_kontor(): + # test kontor without any subcommands or arguments + with KontorTest() as app: + app.run() + assert app.exit_code == 0 + + +def test_kontor_debug(): + # test that debug mode is functional + argv = ['--debug'] + with KontorTest(argv=argv) as app: + app.run() + assert app.debug is True + + +def test_command1(): + # test command1 without arguments + argv = ['command1'] + with KontorTest(argv=argv) as app: + app.run() + data,output = app.last_rendered + assert data['foo'] == 'bar' + assert output.find('Foo => bar') + + + # test command1 with arguments + argv = ['command1', '--foo', 'not-bar'] + with KontorTest(argv=argv) as app: + app.run() + data,output = app.last_rendered + assert data['foo'] == 'not-bar' + assert output.find('Foo => not-bar') diff --git a/python/main.py b/python/main.py new file mode 100644 index 0000000..76d0e8c --- /dev/null +++ b/python/main.py @@ -0,0 +1,16 @@ +# This is a sample Python script. + +# Press Umschalt+F10 to execute it or replace it with your code. +# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings. + + +def print_hi(name): + # Use a breakpoint in the code line below to debug your script. + print(f'Hi, {name}') # Press Strg+F8 to toggle the breakpoint. + + +# Press the green button in the gutter to run the script. +if __name__ == '__main__': + print_hi('PyCharm') + +# See PyCharm help at https://www.jetbrains.com/help/pycharm/ diff --git a/qt/.gitignore b/python/qt/.gitignore similarity index 100% rename from qt/.gitignore rename to python/qt/.gitignore diff --git a/python/qt/database/__init__.py b/python/qt/database/__init__.py new file mode 100644 index 0000000..47f5706 --- /dev/null +++ b/python/qt/database/__init__.py @@ -0,0 +1,212 @@ +import json +from datetime import datetime +from pathlib import Path + +import mariadb +from sqlalchemy import create_engine, select, text, MetaData, join +from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker + +from .base import Base +from .comic import Comic, Artist, Publisher, ComicWork, WorkType, StoryArc, Volume, Issue, TradePaperback +from .tysc import Sport, Team, Card, CardSet, Vendor, Rooster, Player, FieldPosition +from .media import MediaFile +from .metadata import MetaDataTable, MetaDataColumn + + +class KontorDB: + + def __init__(self, db_config): + self.db_conn = mariadb.connect( + host=db_config['mariadb']['host'], + port=db_config['mariadb']['port'], + user=db_config['mariadb']['user'], + password=db_config['mariadb']['password'], + database=db_config['mariadb']['database'] + ) + connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}' + .format( + db_config['mariadb']['user'], + db_config['mariadb']['password'], + db_config['mariadb']['host'], + db_config['mariadb']['port'], + db_config['mariadb']['database'])) + # engine = create_engine(connect_string, echo=True) + engine = create_engine(connect_string) + Base.metadata.create_all(bind=engine) + __session__ = sessionmaker(bind=engine) + self.session = __session__() + self.registry = {} + self.init_registry() + + def init_registry(self): + self.registry['card'] = Card + self.registry['card_set'] = CardSet + self.registry['sport'] = Sport + self.registry['team'] = Team + self.registry['field_position'] = FieldPosition + self.registry['rooster'] = Rooster + self.registry['player'] = Player + self.registry['vendor'] = Vendor + self.registry['artist'] = Artist + self.registry['publisher'] = Publisher + self.registry['comic'] = Comic + self.registry['issue'] = Issue + self.registry['story_arc'] = StoryArc + self.registry['trade_paperback'] = TradePaperback + self.registry['volume'] = Volume + self.registry['comic_work'] = ComicWork + self.registry['worktype'] = WorkType + self.registry['media_file'] = MediaFile + + + def get_table_names(self) -> list: + tables = self.session.query(MetaDataTable).all() + result = [table.table_name for table in tables] + return result + + def get_column_meta_data(self, table_name: str, view_only=True) -> dict: + meta_data = {} + order = 0 + if view_only: + for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name). + filter(MetaDataColumn.is_shown == 1).all()): + meta_data[order] = {'column': column.column_name, 'label': column.column_label, + 'order': column.column_order, 'ref_column': column.ref_column} + order += 1 + else: + for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name).all()): + meta_data[order] = { + 'column': column.column_name, + 'order': column.column_order, + 'ref_column': column.ref_column + } + order += 1 + return meta_data + + def get_filters(self, table_name): + _filter_map = {} + for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name). + filter(MetaDataColumn.show_filter == 1).all()): + _filter_map[column.column_name] = {'label': column.filter_label, 'widget': None} + print(f"retrieved {len(_filter_map)} filters: {_filter_map}") + return _filter_map + + def data(self, table, columns: dict, filters) -> list: + data = [] + entries = [] + if len(filters) == 0: + entries = self.session.query(table).all() + else: + entries = self.session.query(table).filter_by(**filters) + for entry in entries: + row = [] + for order in columns.keys(): + column_name = columns[order]['column'] + if str(column_name).endswith("_id"): + ref_table = column_name[:-3] + # print(f"{ref_table=}") + ref = getattr(entry, ref_table) + value = getattr(ref, "name") + # print(f"{value=}") + row.append(value) + else: + row.append(getattr(entry, column_name)) + # print(repr(row)) + data.append(row) + return data + + def get_data(self, table_name: str, columns: dict, where_clause: str) -> list: + data = [] + cursor = self.db_conn.cursor() + cursor.execute(self.get_statement(table_name, columns, where_clause)) + rows = cursor.fetchall() + print(len(rows)) + for row in rows: + # print(f"KontorDB.get_data: {row}") + data.append(list(row)) + cursor.close() + # print(f"KontorDB.getData: return {len(data)}") + if table_name == 'comic' and len(where_clause) == 0: + data.clear() + comics = self.session.query(Comic).all() + for item in comics: + # print(item) + row = [] + for order in columns.keys(): + column_name = columns[order]['column'] + if str(column_name).endswith("_id"): + ref_table = column_name[:-3] + # print(f"{ref_table=}") + ref = getattr(item, ref_table) + value = getattr(ref, "name") + # print(f"{value=}") + row.append(value) + else: + row.append(getattr(item, column_name)) + # print(repr(row)) + data.append(row) + return data + + def get_statement(self, table: str, header: dict, where_clause): + columns = "" + for index, column in header.items(): + if index > 0: + columns += ", " + columns += column['column'] + if len(columns) == 0: + columns = "*" + statement = f"SELECT {columns} FROM {table} {where_clause}" + print(f"{statement=}") + return statement + + def export_db(self, export_type: str, export_file_name: str, export_table_list: list): + print(f"export DB to {export_file_name} as {export_type}") + db = {} + for table in export_table_list: + columns = self.get_column_meta_data(table, view_only=False) + if table in self.registry: + model = self.registry[table] + else: + print(f"table {table} is not registered") + continue + rows = self.session.query(model).all() + entries = [] + print(f"found {len(rows)} entries") + print(f"found {len(columns)} columns") + for row in rows: + # print(row) + entry = {} + for order in columns: + # print(columns[order]) + column_name = columns[order]['column'] + # print(f"get value {column_name} from {row} of table {table}") + try: + value = getattr(row, column_name) + if isinstance(value, datetime): + entry[column_name] = str(value) + else: + entry[column_name] = value + except AttributeError as error: + print("could not get value") + entries.append(entry) + db[table] = entries + export_file = Path(export_file_name) + match export_type: + case "JSON": + json_dump = json.dumps(db, indent=4) + with open(export_file_name, "w") as dump_file: + dump_file.write(json_dump) + case "YAML": + export_file = Path(export_file_name) + case "SQLite": + export_file = Path(export_file_name) + case _: + print("unknown export type") + if export_file.exists(): + print(f"{export_file} exists") diff --git a/python/qt/database/base.py b/python/qt/database/base.py new file mode 100644 index 0000000..97d2990 --- /dev/null +++ b/python/qt/database/base.py @@ -0,0 +1,18 @@ +from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker, declarative_base + + +class Base(DeclarativeBase): + pass + +# class BaseModel: +# +# @classmethod +# def model_lookup_by_table_name(cls, table_name): +# registry_instance = getattr(cls, "registry") +# for mapper_ in registry_instance.mappers: +# model = mapper_.class_ +# model_class_name = model.__tablename__ +# if model_class_name == table_name: +# return model + +# Base = declarative_base(cls=BaseModel) \ No newline at end of file diff --git a/python/qt/database/comic.py b/python/qt/database/comic.py new file mode 100644 index 0000000..49d222f --- /dev/null +++ b/python/qt/database/comic.py @@ -0,0 +1,130 @@ +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from .base import Base + + +class Publisher(Base): + __tablename__ = "publisher" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), unique=True) + comics = relationship("Comic") + + def __repr__(self): + return f'Publisher({self.id} {self.name})' + + def __str__(self): + return self.__repr__() + + +class Comic(Base): + __tablename__ = 'comic' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + title = Column(String(length=255), unique=True) + publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False) + publisher = relationship("Publisher", back_populates="comics") + current_order = Column(BIT(1)) + completed = Column(BIT(1)) + issues = relationship("Issue") + story_arcs = relationship("StoryArc") + trade_paperbacks = relationship("TradePaperback") + volumes = relationship("Volume") + comic_works = relationship("ComicWork") + + def __repr__(self): + return f'Comic({self.id} {self.version} {self.title} {self.publisher.name})' + + def __str__(self): + return f'{self.title}({self.id})' + + +class Volume(Base): + __tablename__ = "volume" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="volumes") + issues = relationship("Issue") + + +class TradePaperback(Base): + __tablename__ = "trade_paperback" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False) + issue_start = Column(Integer) + issue_end = Column(Integer) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="trade_paperbacks") + + +class StoryArc(Base): + __tablename__ = "story_arc" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="story_arcs") + + +class Issue(Base): + __tablename__ = "issue" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + issue_number = Column(String(255)) + in_stock = Column(BIT(1)) + is_read = Column(BIT(1)) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="issues") + volume_id = Column(String, ForeignKey("volume.id"), nullable=True) + volume = relationship("Volume", back_populates="issues") + + +class Artist(Base): + __tablename__ = "artist" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False) + comic_works = relationship("ComicWork") + + +class WorkType(Base): + __tablename__ = "worktype" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False, unique=True) + comic_works = relationship("ComicWork") + + +class ComicWork(Base): + __tablename__ = "comic_work" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="comic_works") + artist_id = Column(String, ForeignKey("artist.id"), nullable=False) + artist = relationship("Artist", back_populates="comic_works") + work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False) + work_type = relationship("WorkType", back_populates="comic_works") diff --git a/qt/database/media.py b/python/qt/database/media.py similarity index 100% rename from qt/database/media.py rename to python/qt/database/media.py diff --git a/qt/database/metadata.py b/python/qt/database/metadata.py similarity index 100% rename from qt/database/metadata.py rename to python/qt/database/metadata.py diff --git a/python/qt/database/tysc.py b/python/qt/database/tysc.py new file mode 100644 index 0000000..733d7ed --- /dev/null +++ b/python/qt/database/tysc.py @@ -0,0 +1,131 @@ +from sqlalchemy import Column, DateTime, Integer, String, ForeignKey, UniqueConstraint +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from .base import Base + + +class Sport(Base): + __tablename__ = "sport" + __table_args__ = ( + UniqueConstraint("name"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255), nullable=False, index=True, unique=True) + teams = relationship("Team") + positions = relationship("FieldPosition") + + +class Team(Base): + __tablename__ = "team" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255), nullable=False, index=True, unique=True) + short_name = Column(String(255), nullable=False, ) + sport_id = Column(String, ForeignKey("sport.id"), nullable=False) + sport = relationship("Sport", back_populates="teams") + roosters = relationship("Rooster") + + +class FieldPosition(Base): + __tablename__ = "field_position" + __table_args__ = ( + UniqueConstraint("name", "sport_id"), + UniqueConstraint("short_name", "sport_id"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255), nullable=False, index=True) + short_name = Column(String(255), nullable=False) + sport_id = Column(String, ForeignKey("sport.id"), nullable=False, index=True) + sport = relationship("Sport", back_populates="positions") + roosters = relationship("Rooster") + + +class Player(Base): + __tablename__ = "player" + __table_args__ = ( + UniqueConstraint("first_name", "last_name"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + first_name = Column(String(255), nullable=False, index=True) + last_name = Column(String(255), nullable=False, index=True) + roosters = relationship("Rooster") + + def get_full_name(self) -> str: + return f"{self.last_name}, {self.first_name}" + + +class Rooster(Base): + __tablename__ = "rooster" + __table_args__ = ( + UniqueConstraint("year", "team_id", "player_id", "position_id"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + year = Column(Integer) + team_id = Column(String, ForeignKey("team.id"), nullable=False, index=True) + team = relationship("Team", back_populates="roosters") + player_id = Column(String, ForeignKey("player.id"), nullable=False, index=True) + player = relationship("Player", back_populates="roosters") + position_id = Column(String, ForeignKey("field_position.id"), nullable=False, index=True) + position = relationship("FieldPosition", back_populates="roosters") + cards = relationship("Card") + +class Vendor(Base): + __tablename__ = "vendor" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255), nullable=False, unique=True, index=True) + card_sets = relationship("CardSet") + cards = relationship("Card") + + +class CardSet(Base): + __tablename__ = "card_set" + __table_args__ = ( + UniqueConstraint("name", "vendor_id"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255), index=True) + parallel_set = Column(BIT(1)) + insert_set = Column(BIT(1)) + vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True) + vendor = relationship("Vendor", back_populates="card_sets") + cards = relationship("Card") + + +class Card(Base): + __tablename__ = "card" + __table_args__ = ( + UniqueConstraint("card_number", "year", "vendor_id", "card_set_id"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + card_number = Column(Integer, index=True) + year = Column(Integer, index=True) + card_set_id = Column(String, ForeignKey("card_set.id"), nullable=False) + card_set = relationship("CardSet", back_populates="cards") + rooster_id = Column(String, ForeignKey("rooster.id"), nullable=False) + rooster = relationship("Rooster", back_populates="cards") + vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False) + vendor = relationship("Vendor", back_populates="cards") diff --git a/python/qt/gui/__init__.py b/python/qt/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qt/gui/data_view.py b/python/qt/gui/data_view.py similarity index 100% rename from qt/gui/data_view.py rename to python/qt/gui/data_view.py diff --git a/qt/gui/data_view_model.py b/python/qt/gui/data_view_model.py similarity index 100% rename from qt/gui/data_view_model.py rename to python/qt/gui/data_view_model.py diff --git a/qt/gui/dialogs.py b/python/qt/gui/dialogs.py similarity index 100% rename from qt/gui/dialogs.py rename to python/qt/gui/dialogs.py diff --git a/qt/gui/main_window.py b/python/qt/gui/main_window.py similarity index 100% rename from qt/gui/main_window.py rename to python/qt/gui/main_window.py diff --git a/qt/gui/model_config.py b/python/qt/gui/model_config.py similarity index 100% rename from qt/gui/model_config.py rename to python/qt/gui/model_config.py diff --git a/qt/gui/table_model.py b/python/qt/gui/table_model.py similarity index 100% rename from qt/gui/table_model.py rename to python/qt/gui/table_model.py diff --git a/qt/kontor.py b/python/qt/kontor.py similarity index 100% rename from qt/kontor.py rename to python/qt/kontor.py diff --git a/qt/pysidedeploy.spec b/python/qt/pysidedeploy.spec similarity index 100% rename from qt/pysidedeploy.spec rename to python/qt/pysidedeploy.spec diff --git a/qt/res/application-export.png b/python/qt/res/application-export.png similarity index 100% rename from qt/res/application-export.png rename to python/qt/res/application-export.png diff --git a/qt/res/application-import.png b/python/qt/res/application-import.png similarity index 100% rename from qt/res/application-import.png rename to python/qt/res/application-import.png diff --git a/qt/res/arrow-circle-double.png b/python/qt/res/arrow-circle-double.png similarity index 100% rename from qt/res/arrow-circle-double.png rename to python/qt/res/arrow-circle-double.png diff --git a/qt/res/cross.png b/python/qt/res/cross.png similarity index 100% rename from qt/res/cross.png rename to python/qt/res/cross.png diff --git a/qt/res/tick.png b/python/qt/res/tick.png similarity index 100% rename from qt/res/tick.png rename to python/qt/res/tick.png From 276302570f38e525a6a813232dc246cc400e95ba Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Tue, 14 Jan 2025 13:10:24 +0100 Subject: [PATCH 27/91] merged command line and gui app in one command --- python/{cli => }/.gitignore | 3 + python/{cli => }/CHANGELOG.md | 0 python/{cli => }/Dockerfile | 0 python/{cli => }/LICENSE.md | 0 python/{cli => }/MANIFEST.in | 0 python/{cli => }/Makefile | 0 python/{cli => }/README.md | 0 python/cli/kontor/controllers/clibase.py | 60 -------- python/cli/kontor/database/base.py | 20 --- python/{cli => }/config/kontor.yml.example | 0 python/{cli => }/docs/.gitkeep | 0 python/{cli => }/kontor/__init__.py | 0 .../{cli => }/kontor/controllers/__init__.py | 0 python/kontor/controllers/clibase.py | 42 ++++++ python/kontor/controllers/database.py | 32 ++++ python/{cli => }/kontor/core/__init__.py | 0 python/{cli => }/kontor/core/exc.py | 0 python/{cli => }/kontor/core/version.py | 0 python/{cli => }/kontor/database/__init__.py | 123 ++++++---------- python/kontor/database/base.py | 5 + python/{cli => }/kontor/database/comic.py | 0 python/{cli => }/kontor/database/media.py | 0 python/{cli => }/kontor/database/metadata.py | 1 + python/{qt => kontor}/database/tysc.py | 0 python/{cli => }/kontor/ext/__init__.py | 0 .../kontor/plugins => kontor/gui}/__init__.py | 0 python/{qt => kontor}/gui/data_view.py | 0 python/{qt => kontor}/gui/data_view_model.py | 0 python/{qt => kontor}/gui/dialogs.py | 0 python/kontor/gui/main_window.py | 139 ++++++++++++++++++ python/kontor/gui/model_config.py | 50 +++++++ python/kontor/gui/table_model.py | 108 ++++++++++++++ python/{cli => }/kontor/main.py | 14 +- .../templates => kontor/plugins}/__init__.py | 0 .../{qt => kontor}/res/application-export.png | Bin .../{qt => kontor}/res/application-import.png | Bin .../res/arrow-circle-double.png | Bin python/{qt => kontor}/res/cross.png | Bin python/{qt => kontor}/res/tick.png | Bin .../{qt/gui => kontor/templates}/__init__.py | 0 .../kontor/templates/command1.jinja2 | 0 python/main.py | 16 -- python/{cli => }/requirements-dev.txt | 0 python/{cli => }/requirements.txt | 1 + python/{cli => }/setup.cfg | 0 python/{cli => }/setup.py | 0 python/{cli => }/tests/conftest.py | 0 python/{cli => }/tests/test_kontor.py | 0 {python/qt => qt}/.gitignore | 0 {python/qt => qt}/database/__init__.py | 0 {python/qt => qt}/database/base.py | 0 {python/qt => qt}/database/comic.py | 0 {python/qt => qt}/database/media.py | 0 {python/qt => qt}/database/metadata.py | 0 {python/cli/kontor => qt}/database/tysc.py | 8 +- qt/gui/__init__.py | 0 qt/gui/data_view.py | 12 ++ qt/gui/data_view_model.py | 32 ++++ qt/gui/dialogs.py | 106 +++++++++++++ {python/qt => qt}/gui/main_window.py | 0 {python/qt => qt}/gui/model_config.py | 0 {python/qt => qt}/gui/table_model.py | 0 {python/qt => qt}/kontor.py | 0 {python/qt => qt}/pysidedeploy.spec | 0 qt/res/application-export.png | Bin 0 -> 513 bytes qt/res/application-import.png | Bin 0 -> 524 bytes qt/res/arrow-circle-double.png | Bin 0 -> 836 bytes qt/res/cross.png | Bin 0 -> 544 bytes qt/res/tick.png | Bin 0 -> 634 bytes 69 files changed, 589 insertions(+), 183 deletions(-) rename python/{cli => }/.gitignore (98%) rename python/{cli => }/CHANGELOG.md (100%) rename python/{cli => }/Dockerfile (100%) rename python/{cli => }/LICENSE.md (100%) rename python/{cli => }/MANIFEST.in (100%) rename python/{cli => }/Makefile (100%) rename python/{cli => }/README.md (100%) delete mode 100644 python/cli/kontor/controllers/clibase.py delete mode 100644 python/cli/kontor/database/base.py rename python/{cli => }/config/kontor.yml.example (100%) rename python/{cli => }/docs/.gitkeep (100%) rename python/{cli => }/kontor/__init__.py (100%) rename python/{cli => }/kontor/controllers/__init__.py (100%) create mode 100644 python/kontor/controllers/clibase.py create mode 100644 python/kontor/controllers/database.py rename python/{cli => }/kontor/core/__init__.py (100%) rename python/{cli => }/kontor/core/exc.py (100%) rename python/{cli => }/kontor/core/version.py (100%) rename python/{cli => }/kontor/database/__init__.py (55%) create mode 100644 python/kontor/database/base.py rename python/{cli => }/kontor/database/comic.py (100%) rename python/{cli => }/kontor/database/media.py (100%) rename python/{cli => }/kontor/database/metadata.py (99%) rename python/{qt => kontor}/database/tysc.py (100%) rename python/{cli => }/kontor/ext/__init__.py (100%) rename python/{cli/kontor/plugins => kontor/gui}/__init__.py (100%) rename python/{qt => kontor}/gui/data_view.py (100%) rename python/{qt => kontor}/gui/data_view_model.py (100%) rename python/{qt => kontor}/gui/dialogs.py (100%) create mode 100644 python/kontor/gui/main_window.py create mode 100644 python/kontor/gui/model_config.py create mode 100644 python/kontor/gui/table_model.py rename python/{cli => }/kontor/main.py (91%) rename python/{cli/kontor/templates => kontor/plugins}/__init__.py (100%) rename python/{qt => kontor}/res/application-export.png (100%) rename python/{qt => kontor}/res/application-import.png (100%) rename python/{qt => kontor}/res/arrow-circle-double.png (100%) rename python/{qt => kontor}/res/cross.png (100%) rename python/{qt => kontor}/res/tick.png (100%) rename python/{qt/gui => kontor/templates}/__init__.py (100%) rename python/{cli => }/kontor/templates/command1.jinja2 (100%) delete mode 100644 python/main.py rename python/{cli => }/requirements-dev.txt (100%) rename python/{cli => }/requirements.txt (90%) rename python/{cli => }/setup.cfg (100%) rename python/{cli => }/setup.py (100%) rename python/{cli => }/tests/conftest.py (100%) rename python/{cli => }/tests/test_kontor.py (100%) rename {python/qt => qt}/.gitignore (100%) rename {python/qt => qt}/database/__init__.py (100%) rename {python/qt => qt}/database/base.py (100%) rename {python/qt => qt}/database/comic.py (100%) rename {python/qt => qt}/database/media.py (100%) rename {python/qt => qt}/database/metadata.py (100%) rename {python/cli/kontor => qt}/database/tysc.py (94%) create mode 100644 qt/gui/__init__.py create mode 100644 qt/gui/data_view.py create mode 100644 qt/gui/data_view_model.py create mode 100644 qt/gui/dialogs.py rename {python/qt => qt}/gui/main_window.py (100%) rename {python/qt => qt}/gui/model_config.py (100%) rename {python/qt => qt}/gui/table_model.py (100%) rename {python/qt => qt}/kontor.py (100%) rename {python/qt => qt}/pysidedeploy.spec (100%) mode change 100755 => 100644 create mode 100644 qt/res/application-export.png create mode 100644 qt/res/application-import.png create mode 100644 qt/res/arrow-circle-double.png create mode 100644 qt/res/cross.png create mode 100644 qt/res/tick.png diff --git a/python/cli/.gitignore b/python/.gitignore similarity index 98% rename from python/cli/.gitignore rename to python/.gitignore index a74b246..0f47561 100644 --- a/python/cli/.gitignore +++ b/python/.gitignore @@ -95,6 +95,9 @@ venv.bak/ .spyderproject .spyproject +# PyCharm +.idea/ + # Rope project settings .ropeproject diff --git a/python/cli/CHANGELOG.md b/python/CHANGELOG.md similarity index 100% rename from python/cli/CHANGELOG.md rename to python/CHANGELOG.md diff --git a/python/cli/Dockerfile b/python/Dockerfile similarity index 100% rename from python/cli/Dockerfile rename to python/Dockerfile diff --git a/python/cli/LICENSE.md b/python/LICENSE.md similarity index 100% rename from python/cli/LICENSE.md rename to python/LICENSE.md diff --git a/python/cli/MANIFEST.in b/python/MANIFEST.in similarity index 100% rename from python/cli/MANIFEST.in rename to python/MANIFEST.in diff --git a/python/cli/Makefile b/python/Makefile similarity index 100% rename from python/cli/Makefile rename to python/Makefile diff --git a/python/cli/README.md b/python/README.md similarity index 100% rename from python/cli/README.md rename to python/README.md diff --git a/python/cli/kontor/controllers/clibase.py b/python/cli/kontor/controllers/clibase.py deleted file mode 100644 index 8d69fb2..0000000 --- a/python/cli/kontor/controllers/clibase.py +++ /dev/null @@ -1,60 +0,0 @@ - -from cement import Controller, ex -from cement.utils.version import get_version_banner -from ..core.version import get_version - -VERSION_BANNER = """ -Kontor CLI Tool %s -%s -""" % (get_version(), get_version_banner()) - - -class CliBase(Controller): - class Meta: - label = 'base' - - # text displayed at the top of --help output - description = 'Kontor CLI Tool' - - # text displayed at the bottom of --help output - epilog = 'Usage: kontor command1 --foo bar' - - # controller level arguments. ex: 'kontor --version' - arguments = [ - ### add a version banner - ( [ '-v', '--version' ], - { 'action' : 'version', - 'version' : VERSION_BANNER } ), - ] - - - def _default(self): - """Default action if no sub-command is passed.""" - - self.app.args.print_help() - - - @ex( - help='example sub command1', - - # sub-command level arguments. ex: 'kontor command1 --foo bar' - arguments=[ - ### add a sample foo option under subcommand namespace - ( [ '-f', '--foo' ], - { 'help' : 'notorious foo option', - 'action' : 'store', - 'dest' : 'foo' } ), - ], - ) - def command1(self): - """Example sub-command.""" - - data = { - 'foo' : 'bar', - } - - ### do something with arguments - if self.app.pargs.foo is not None: - data['foo'] = self.app.pargs.foo - - self.app.render(data, 'command1.jinja2') diff --git a/python/cli/kontor/database/base.py b/python/cli/kontor/database/base.py deleted file mode 100644 index f97d724..0000000 --- a/python/cli/kontor/database/base.py +++ /dev/null @@ -1,20 +0,0 @@ -from sqlalchemy import Column, String, DateTime, Integer -from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker, declarative_base - - -# class Base(DeclarativeBase): -# pass - -class BaseModel: - - @classmethod - def model_lookup_by_table_name(cls, table_name): - registry_instance = getattr(cls, "registry") - for mapper_ in registry_instance.mappers: - model = mapper_.class_ - model_class_name = model.__tablename__ - if model_class_name == table_name: - return model - - -Base = declarative_base(cls=BaseModel) \ No newline at end of file diff --git a/python/cli/config/kontor.yml.example b/python/config/kontor.yml.example similarity index 100% rename from python/cli/config/kontor.yml.example rename to python/config/kontor.yml.example diff --git a/python/cli/docs/.gitkeep b/python/docs/.gitkeep similarity index 100% rename from python/cli/docs/.gitkeep rename to python/docs/.gitkeep diff --git a/python/cli/kontor/__init__.py b/python/kontor/__init__.py similarity index 100% rename from python/cli/kontor/__init__.py rename to python/kontor/__init__.py diff --git a/python/cli/kontor/controllers/__init__.py b/python/kontor/controllers/__init__.py similarity index 100% rename from python/cli/kontor/controllers/__init__.py rename to python/kontor/controllers/__init__.py diff --git a/python/kontor/controllers/clibase.py b/python/kontor/controllers/clibase.py new file mode 100644 index 0000000..568a546 --- /dev/null +++ b/python/kontor/controllers/clibase.py @@ -0,0 +1,42 @@ +from PySide6.QtWidgets import QApplication +from cement import Controller, ex +from cement.utils.version import get_version_banner +from ..core.version import get_version +from ..gui.main_window import MainWindow + +VERSION_BANNER = """ +Kontor CLI Tool %s +%s +""" % (get_version(), get_version_banner()) + + +class CliBase(Controller): + class Meta: + label = 'base' + + # text displayed at the top of --help output + description = 'Kontor CLI Tool' + + # text displayed at the bottom of --help output + epilog = 'Usage: kontor gui|database' + + # controller level arguments. ex: 'kontor --version' + arguments = [ + ### add a version banner + (['-v', '--version'], + {'action': 'version', + 'version': VERSION_BANNER}), + ] + + def _default(self): + """Default action if no sub-command is passed.""" + self.gui() + + @ex( + help='start GUI' + ) + def gui(self): + application = QApplication([]) + window = MainWindow(self.app.session, self.app.log) + window.show() + application.exec() diff --git a/python/kontor/controllers/database.py b/python/kontor/controllers/database.py new file mode 100644 index 0000000..246d4f8 --- /dev/null +++ b/python/kontor/controllers/database.py @@ -0,0 +1,32 @@ +from cement import Controller, ex + +from ..database import KontorDB + + +class Database(Controller): + + class Meta: + label = 'database' + stacked_type = 'nested' + stacked_on = 'base' + + @ex( + help='export database to given file', + arguments=[ + (['-f', '--file'], + {'help': 'file to store database content', + 'action': 'store', + 'dest': 'db_file'}) + ], + ) + def export(self): + data = { + 'db_file': 'data.json', + 'export_type': 'JSON', + } + if self.app.pargs.db_file is not None: + data['db_file'] = self.app.pargs.db_file + kontor_db = KontorDB(self.app.session, self.app.log) + table_list = kontor_db.get_table_names() + kontor_db.export_db(data['export_type'], data['db_file'], table_list) + self.app.render(data, 'command1.jinja2') diff --git a/python/cli/kontor/core/__init__.py b/python/kontor/core/__init__.py similarity index 100% rename from python/cli/kontor/core/__init__.py rename to python/kontor/core/__init__.py diff --git a/python/cli/kontor/core/exc.py b/python/kontor/core/exc.py similarity index 100% rename from python/cli/kontor/core/exc.py rename to python/kontor/core/exc.py diff --git a/python/cli/kontor/core/version.py b/python/kontor/core/version.py similarity index 100% rename from python/cli/kontor/core/version.py rename to python/kontor/core/version.py diff --git a/python/cli/kontor/database/__init__.py b/python/kontor/database/__init__.py similarity index 55% rename from python/cli/kontor/database/__init__.py rename to python/kontor/database/__init__.py index 95ce1d8..c7e6cb5 100644 --- a/python/cli/kontor/database/__init__.py +++ b/python/kontor/database/__init__.py @@ -2,37 +2,40 @@ import json from datetime import datetime from pathlib import Path -import mariadb -from sqlalchemy import create_engine, select, text, MetaData, join -from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker - from .base import Base -from .comic import Comic +from .comic import Comic, Artist, Publisher, Issue, StoryArc, TradePaperback, Volume, ComicWork, WorkType from .metadata import MetaDataTable, MetaDataColumn +from .tysc import Card, CardSet, Sport, Team, FieldPosition, Rooster, Player, Vendor +from .media import MediaFile class KontorDB: - def __init__(self, db_config): - self.db_conn = mariadb.connect( - host=db_config['mariadb']['host'], - port=db_config['mariadb']['port'], - user=db_config['mariadb']['user'], - password=db_config['mariadb']['password'], - database=db_config['mariadb']['database'] - ) - connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}' - .format( - db_config['mariadb']['user'], - db_config['mariadb']['password'], - db_config['mariadb']['host'], - db_config['mariadb']['port'], - db_config['mariadb']['database'])) - # engine = create_engine(connect_string, echo=True) - engine = create_engine(connect_string) - Base.metadata.create_all(bind=engine) - __session__ = sessionmaker(bind=engine) - self.session = __session__() + def __init__(self, db_session, log): + self.session = db_session + self.log = log + self.registry = {} + self.init_registry() + + def init_registry(self): + self.registry['card'] = Card + self.registry['card_set'] = CardSet + self.registry['sport'] = Sport + self.registry['team'] = Team + self.registry['field_position'] = FieldPosition + self.registry['rooster'] = Rooster + self.registry['player'] = Player + self.registry['vendor'] = Vendor + self.registry['artist'] = Artist + self.registry['publisher'] = Publisher + self.registry['comic'] = Comic + self.registry['issue'] = Issue + self.registry['story_arc'] = StoryArc + self.registry['trade_paperback'] = TradePaperback + self.registry['volume'] = Volume + self.registry['comic_work'] = ComicWork + self.registry['worktype'] = WorkType + self.registry['media_file'] = MediaFile def get_table_names(self) -> list: tables = self.session.query(MetaDataTable).all() @@ -69,7 +72,7 @@ class KontorDB: filter(MetaDataTable.table_name == table_name). filter(MetaDataColumn.show_filter == 1).all()): _filter_map[column.column_name] = {'label': column.filter_label, 'widget': None} - print(f"retrieved {len(_filter_map)} filters: {_filter_map}") + self.log.info(f"retrieved {len(_filter_map)} filters: {_filter_map}") return _filter_map def data(self, table, columns: dict, filters) -> list: @@ -96,67 +99,27 @@ class KontorDB: data.append(row) return data - def get_data(self, table_name: str, columns: dict, where_clause: str) -> list: - data = [] - cursor = self.db_conn.cursor() - cursor.execute(self.get_statement(table_name, columns, where_clause)) - rows = cursor.fetchall() - print(len(rows)) - for row in rows: - # print(f"KontorDB.get_data: {row}") - data.append(list(row)) - cursor.close() - # print(f"KontorDB.getData: return {len(data)}") - if table_name == 'comic' and len(where_clause) == 0: - data.clear() - comics = self.session.query(Comic).all() - for item in comics: - # print(item) - row = [] - for order in columns.keys(): - column_name = columns[order]['column'] - if str(column_name).endswith("_id"): - ref_table = column_name[:-3] - # print(f"{ref_table=}") - ref = getattr(item, ref_table) - value = getattr(ref, "name") - # print(f"{value=}") - row.append(value) - else: - row.append(getattr(item, column_name)) - # print(repr(row)) - data.append(row) - return data - - def get_statement(self, table: str, header: dict, where_clause): - columns = "" - for index, column in header.items(): - if index > 0: - columns += ", " - columns += column['column'] - if len(columns) == 0: - columns = "*" - statement = f"SELECT {columns} FROM {table} {where_clause}" - print(f"{statement=}") - return statement - def export_db(self, export_type: str, export_file_name: str, export_table_list: list): - print(f"export DB to {export_file_name} as {export_type}") + self.log.info(f"export DB to {export_file_name} as {export_type}") db = {} for table in export_table_list: columns = self.get_column_meta_data(table, view_only=False) - model = Base.model_lookup_by_table_name(table) + if table in self.registry: + model = self.registry[table] + else: + print(f"table {table} is not registered") + continue rows = self.session.query(model).all() entries = [] - print(f"found {len(rows)} entries") - print(f"found {len(columns)} columns") + self.log.debug(f"found {len(rows)} entries") + self.log.debug(f"found {len(columns)} columns") for row in rows: - print(row) + # print(row) entry = {} for order in columns: - print(columns[order]) + # print(columns[order]) column_name = columns[order]['column'] - print(f"get value {column_name} from {row} of table {table}") + # print(f"get value {column_name} from {row} of table {table}") try: value = getattr(row, column_name) if isinstance(value, datetime): @@ -164,7 +127,7 @@ class KontorDB: else: entry[column_name] = value except AttributeError as error: - print("could not get value") + self.log.debug("could not get value") entries.append(entry) db[table] = entries export_file = Path(export_file_name) @@ -178,6 +141,6 @@ class KontorDB: case "SQLite": export_file = Path(export_file_name) case _: - print("unknown export type") + self.log.debug("unknown export type") if export_file.exists(): - print(f"{export_file} exists") + self.log.debug(f"{export_file} exists") diff --git a/python/kontor/database/base.py b/python/kontor/database/base.py new file mode 100644 index 0000000..fa2b68a --- /dev/null +++ b/python/kontor/database/base.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import DeclarativeBase + + +class Base(DeclarativeBase): + pass diff --git a/python/cli/kontor/database/comic.py b/python/kontor/database/comic.py similarity index 100% rename from python/cli/kontor/database/comic.py rename to python/kontor/database/comic.py diff --git a/python/cli/kontor/database/media.py b/python/kontor/database/media.py similarity index 100% rename from python/cli/kontor/database/media.py rename to python/kontor/database/media.py diff --git a/python/cli/kontor/database/metadata.py b/python/kontor/database/metadata.py similarity index 99% rename from python/cli/kontor/database/metadata.py rename to python/kontor/database/metadata.py index 2975e10..d7dd4c0 100644 --- a/python/cli/kontor/database/metadata.py +++ b/python/kontor/database/metadata.py @@ -20,6 +20,7 @@ class MetaDataTable(Base): def __str__(self): return f'{self.table_name}({self.id})' + class MetaDataColumn(Base): __tablename__ = 'meta_data_column' id = Column(String, primary_key=True) diff --git a/python/qt/database/tysc.py b/python/kontor/database/tysc.py similarity index 100% rename from python/qt/database/tysc.py rename to python/kontor/database/tysc.py diff --git a/python/cli/kontor/ext/__init__.py b/python/kontor/ext/__init__.py similarity index 100% rename from python/cli/kontor/ext/__init__.py rename to python/kontor/ext/__init__.py diff --git a/python/cli/kontor/plugins/__init__.py b/python/kontor/gui/__init__.py similarity index 100% rename from python/cli/kontor/plugins/__init__.py rename to python/kontor/gui/__init__.py diff --git a/python/qt/gui/data_view.py b/python/kontor/gui/data_view.py similarity index 100% rename from python/qt/gui/data_view.py rename to python/kontor/gui/data_view.py diff --git a/python/qt/gui/data_view_model.py b/python/kontor/gui/data_view_model.py similarity index 100% rename from python/qt/gui/data_view_model.py rename to python/kontor/gui/data_view_model.py diff --git a/python/qt/gui/dialogs.py b/python/kontor/gui/dialogs.py similarity index 100% rename from python/qt/gui/dialogs.py rename to python/kontor/gui/dialogs.py diff --git a/python/kontor/gui/main_window.py b/python/kontor/gui/main_window.py new file mode 100644 index 0000000..24a84a6 --- /dev/null +++ b/python/kontor/gui/main_window.py @@ -0,0 +1,139 @@ +from PySide6.QtGui import QAction, QIcon +from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView +from PySide6.QtWidgets import QLabel, QMainWindow + +from ..database import KontorDB +from ..database.media import MediaFile +from ..database.comic import Comic +from .dialogs import ExportKontorDialog, ImportKontorDialog +from .model_config import KontorModelConfig +from .table_model import KontorTableModel + + +class MainWindow(QMainWindow): + + def __init__(self, session, log): + super().__init__() + + self.tick = QIcon('kontor/res/tick.png') + self.cross = QIcon('kontor/res/cross.png') + self.import_icon = QIcon("kontor/res/application-import.png") + self.export_icon = QIcon("kontor/res/application-export.png") + self.circle_icon = QIcon("kontor/res/arrow-circle-double.png") + + self.setWindowTitle("Kontor") + self.setMinimumSize(800, 500) + self._create_actions() + self._create_menubar() + self._create_toolbars() + self._create_statusbar() + + self.data = [] + self.filter = {} + self.kontor_db = KontorDB(session, log) + self.log = log + self.central_widget = QWidget() + parent_layout = QVBoxLayout() + self.central_widget.setLayout(parent_layout) + self.tabs = QTabWidget() + self.tabs.addTab(self.generate_data_tab("comic", Comic), "Comics") + self.tabs.addTab(self.generate_data_tab("media_file", MediaFile), "MediaFile") + self.tabs.currentChanged.connect(self._tab_changed) + #label.setAlignment(Qt.AlignmentFlag.AlignCenter) + parent_layout.addWidget(self.tabs) + + self.setCentralWidget(self.central_widget) + + def _create_actions(self): + self.newAction = QAction("&New", self) + self.aboutAction = QAction("&Über...", self) + self.aboutAction.triggered.connect(self.about) + self.importAction = QAction(self.import_icon, "&Import", self) + self.importAction.triggered.connect(self.import_from_file) + self.exportAction = QAction(self.export_icon, "&Export", self) + self.exportAction.triggered.connect(self.export_to_file) + self.refreshAction = QAction(self.circle_icon, "&Refresh", self) + self.refreshAction.triggered.connect(self.refresh) + self.updateTitleAction = QAction("&Update Titles", self) + self.downloadAction = QAction("&Download Videos", self) + self.exitAction = QAction("&Beenden", self) + self.exitAction.setShortcut("Alt+F4") + self.exitAction.triggered.connect(self.close) + + def _create_menubar(self): + menu_bar = self.menuBar() + # File menu + file_menu = QMenu("&Datei") + menu_bar.addMenu(file_menu) + file_menu.addAction(self.exitAction) + # Kontor menu + kontor_menu = QMenu("&Kontor") + menu_bar.addMenu(kontor_menu) + kontor_menu.addAction(self.importAction) + kontor_menu.addAction(self.exportAction) + comic_menu = QMenu("&Comic") + tysc_menu = QMenu("&TradeYourSportCards") + media_file_menu = QMenu("&MediaFile") + media_file_menu.addAction(self.updateTitleAction) + media_file_menu.addAction(self.downloadAction) + kontor_menu.addMenu(comic_menu) + kontor_menu.addMenu(tysc_menu) + kontor_menu.addMenu(media_file_menu) + # Help menu + help_menu = QMenu("&Hilfe") + menu_bar.addMenu(help_menu) + help_menu.addAction(self.aboutAction) + + def _create_toolbars(self): + # Kontor toolbar + kontor_tool_bar = self.addToolBar("Kontor") + kontor_tool_bar.addAction(self.importAction) + kontor_tool_bar.addAction(self.exportAction) + kontor_tool_bar.addAction(self.refreshAction) + + def _create_statusbar(self): + self.statusBar = self.statusBar() + self.statusBar.showMessage("Kontor ready", 6000) + self.status_label = QLabel("") + self.statusBar.addPermanentWidget(self.status_label) + + def about(self): + QMessageBox.about(self.central_widget, "Über Kontor", f"Python: 3.11\nKontor: 0.1.0") + + def import_from_file(self): + import_dlg = ImportKontorDialog(self) + if import_dlg.exec(): + print(f"import DB from file {import_dlg.file_name}") + else: + print("no nothing for import") + pass + + def export_to_file(self): + export_dlg = ExportKontorDialog(self, self.kontor_db) + if export_dlg.exec(): + self.log.info(export_dlg.get_tables_to_export()) + self.log.info(f"export DB to {export_dlg.file_name}") + self.statusBar.showMessage(f"export DB to {export_dlg.file_name}", 3000) + self.kontor_db.export_db(export_dlg.current_export_type, export_dlg.file_name, export_dlg.get_tables_to_export()) + else: + self.statusBar.showMessage("Export cancelled", 3000) + + def refresh(self): + self.data[self.tabs.currentIndex()].refresh() + + def _tab_changed(self, tab_index): + self.data[tab_index].refresh() + + def generate_data_tab(self, table_name, table): + data_tab = QWidget() + table_config = KontorModelConfig(self.kontor_db, self, table_name, table) + model = KontorTableModel(table_config) + layout = QVBoxLayout() + self.data.append(model) + data_tab.setLayout(layout) + table_view = QTableView() + table_view.setModel(model) + layout.addLayout(table_config.get_filter_layout()) + layout.addWidget(table_view) + model.refresh() + return data_tab diff --git a/python/kontor/gui/model_config.py b/python/kontor/gui/model_config.py new file mode 100644 index 0000000..ce61107 --- /dev/null +++ b/python/kontor/gui/model_config.py @@ -0,0 +1,50 @@ +import mariadb +from PySide6.QtWidgets import QHBoxLayout, QCheckBox + +from ..database import KontorDB + + +class KontorModelConfig: + + def __init__(self, kontor_db: KontorDB, main_window, table_name: str, table): + self.header = {} + self.filter = {} + self.main_window = main_window + self._table_name = table_name + self._table = table + self.kontor_db = kontor_db + self.get_table_config() + + def get_table_config(self): + self.header = self.kontor_db.get_column_meta_data(self._table_name) + self.filter = self.kontor_db.get_filters(self._table_name) + + def filters(self) -> dict: + _filters = {} + # print(self.filter["download"].isChecked()) + for column, filter_info in self.filter.items(): + # print(column, filter_info) + if filter_info['widget'].isChecked(): + _filters[column] = True + # print(f"{filter_rule=}") + return _filters + + def get_data(self) -> list: + # data = self.kontor_db.get_data(self._table_name, self.header, self.get_filter()) + # data.clear() + data = self.kontor_db.data(self._table, self.header, self.filters()) + # print(f"KontorModelConfig.get_data: {len(data)}") + # comics = self.kontor_db.session.query(Comic).all() + # print(f'{len(comics)} Comics loaded') + return data + + def get_filter_layout(self) -> QHBoxLayout: + filter_layout = QHBoxLayout() + for column, filter_info in self.filter.items(): + filter_checkbox = QCheckBox() + filter_checkbox.setText(filter_info['label']) + filter_checkbox.checkStateChanged.connect(self.main_window.refresh) + self.filter[column]['widget'] = filter_checkbox + filter_layout.addWidget(filter_checkbox) + filter_layout.addStretch() + return filter_layout diff --git a/python/kontor/gui/table_model.py b/python/kontor/gui/table_model.py new file mode 100644 index 0000000..cd08ce1 --- /dev/null +++ b/python/kontor/gui/table_model.py @@ -0,0 +1,108 @@ +from datetime import datetime + +from PySide6.QtCore import QAbstractTableModel, QModelIndex +from PySide6.QtGui import Qt + +from .model_config import KontorModelConfig + + +class KontorTableModel(QAbstractTableModel): + + def __init__(self, model_config: KontorModelConfig): + super().__init__() + self._main_window = model_config.main_window + self._config = model_config + self._data = [] + + def refresh(self): + data = self._config.get_data() + count = 0 + # print(data) + if data is not None: + self.beginResetModel() + self._data.clear() + self._data = data + self.endResetModel() + count = len(data) + # print(data) + # print(self._data) + self.layoutChanged.emit() + self._main_window.statusBar.showMessage(f"{count} Einträge geladen", 3000) + + def rowCount(self, parent=QModelIndex()): + # The length of the outer list. + if self._data is None: + return 0 + return len(self._data) + + def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): + if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: + return self._config.header[col]['label'] + if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: + return str(col+1) + + def data(self, index, role=Qt.ItemDataRole.DisplayRole): + if self._data is None: + return None + value = self._data[index.row()][index.column()] + # print('{}:: {}:: {}: {}'.format(index, role, value, type(value))) + if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole: + if isinstance(value, datetime): + return value.strftime("%Y-%m-%d %M:%M:%S") + if isinstance(value, str): + return value + if isinstance(value, bytes): + if value == b'\x01': + return self._main_window.tick + else: + return self._main_window.cross + if isinstance(value, int): + # print('{}:: {}: {}'.format(index, value, type(value))) + if value == 1: + return self._main_window.tick + else: + return self._main_window.cross + if isinstance(value, bool): + if value: + return self._main_window.tick + else: + return self._main_window.cross + return str(value) + if role == Qt.ItemDataRole.DecorationRole: + if isinstance(value, bytes): + if value == b'\x01': + return self._main_window.tick + else: + return self._main_window.cross + if isinstance(value, int): + if value == 1: + return self._main_window.tick + else: + return self._main_window.cross + if isinstance(value, bool): + if value: + return self._main_window.tick + else: + return self._main_window.cross + + def columnCount(self, index=QModelIndex()): + # The following takes the first sub-list, and returns + # the length (only works if all rows are an equal length) + # print(f"Header count: {len(self._config.get_header())}") + return len(self._config.header) + + def setData(self, index, value, role: int) -> bool: + print(index, role) + if role == Qt.ItemDataRole.EditRole: + self._data[index.row()][index.column()] = value + print(self._data[index.row()][index.column()]) + self.dataChanged.emit(index, index) + return True + if role == Qt.ItemDataRole.CheckStateRole: + print("role == Qt.ItemDataRole.CheckStateRole") + checked = value == Qt.CheckState.Checked + self._data[index.row()][index.column()] = checked + return False + + def flags(self, index): + return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate diff --git a/python/cli/kontor/main.py b/python/kontor/main.py similarity index 91% rename from python/cli/kontor/main.py rename to python/kontor/main.py index 4c71aab..aa71e06 100644 --- a/python/cli/kontor/main.py +++ b/python/kontor/main.py @@ -1,4 +1,3 @@ - from cement import App, TestApp, init_defaults from cement.core.exc import CaughtSignal from sqlalchemy import create_engine @@ -7,6 +6,7 @@ from sqlalchemy.orm import sessionmaker from .core.exc import KontorError from .database.base import Base from .controllers.clibase import CliBase +from .controllers.database import Database # configuration defaults CONFIG = init_defaults('kontor', 'mariadb') @@ -17,6 +17,7 @@ CONFIG['mariadb']['host'] = '127.0.0.1' CONFIG['mariadb']['port'] = '3306' CONFIG['mariadb']['database'] = 'kontor' + def extend_sqlalchemy(app): app.log.info('extending kontor application with sqlalchemy') connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format( @@ -33,6 +34,11 @@ def extend_sqlalchemy(app): app.extend('session', __session__()) +def close_session(app): + app.log.info('close session') + app.session.close() + + class Kontor(App): """Kontor primary application.""" @@ -66,14 +72,16 @@ class Kontor(App): hooks = [ ('post_setup', extend_sqlalchemy), + ('pre_close', close_session), ] # register handlers handlers = [ - CliBase + CliBase, + Database, ] -class KontorTest(TestApp,Kontor): +class KontorTest(TestApp, Kontor): """A sub-class of Kontor that is better suited for testing.""" class Meta: diff --git a/python/cli/kontor/templates/__init__.py b/python/kontor/plugins/__init__.py similarity index 100% rename from python/cli/kontor/templates/__init__.py rename to python/kontor/plugins/__init__.py diff --git a/python/qt/res/application-export.png b/python/kontor/res/application-export.png similarity index 100% rename from python/qt/res/application-export.png rename to python/kontor/res/application-export.png diff --git a/python/qt/res/application-import.png b/python/kontor/res/application-import.png similarity index 100% rename from python/qt/res/application-import.png rename to python/kontor/res/application-import.png diff --git a/python/qt/res/arrow-circle-double.png b/python/kontor/res/arrow-circle-double.png similarity index 100% rename from python/qt/res/arrow-circle-double.png rename to python/kontor/res/arrow-circle-double.png diff --git a/python/qt/res/cross.png b/python/kontor/res/cross.png similarity index 100% rename from python/qt/res/cross.png rename to python/kontor/res/cross.png diff --git a/python/qt/res/tick.png b/python/kontor/res/tick.png similarity index 100% rename from python/qt/res/tick.png rename to python/kontor/res/tick.png diff --git a/python/qt/gui/__init__.py b/python/kontor/templates/__init__.py similarity index 100% rename from python/qt/gui/__init__.py rename to python/kontor/templates/__init__.py diff --git a/python/cli/kontor/templates/command1.jinja2 b/python/kontor/templates/command1.jinja2 similarity index 100% rename from python/cli/kontor/templates/command1.jinja2 rename to python/kontor/templates/command1.jinja2 diff --git a/python/main.py b/python/main.py deleted file mode 100644 index 76d0e8c..0000000 --- a/python/main.py +++ /dev/null @@ -1,16 +0,0 @@ -# This is a sample Python script. - -# Press Umschalt+F10 to execute it or replace it with your code. -# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings. - - -def print_hi(name): - # Use a breakpoint in the code line below to debug your script. - print(f'Hi, {name}') # Press Strg+F8 to toggle the breakpoint. - - -# Press the green button in the gutter to run the script. -if __name__ == '__main__': - print_hi('PyCharm') - -# See PyCharm help at https://www.jetbrains.com/help/pycharm/ diff --git a/python/cli/requirements-dev.txt b/python/requirements-dev.txt similarity index 100% rename from python/cli/requirements-dev.txt rename to python/requirements-dev.txt diff --git a/python/cli/requirements.txt b/python/requirements.txt similarity index 90% rename from python/cli/requirements.txt rename to python/requirements.txt index bee2df1..06a15f9 100644 --- a/python/cli/requirements.txt +++ b/python/requirements.txt @@ -4,3 +4,4 @@ cement[yaml] cement[colorlog] mariadb sqlalchemy +PySide6 diff --git a/python/cli/setup.cfg b/python/setup.cfg similarity index 100% rename from python/cli/setup.cfg rename to python/setup.cfg diff --git a/python/cli/setup.py b/python/setup.py similarity index 100% rename from python/cli/setup.py rename to python/setup.py diff --git a/python/cli/tests/conftest.py b/python/tests/conftest.py similarity index 100% rename from python/cli/tests/conftest.py rename to python/tests/conftest.py diff --git a/python/cli/tests/test_kontor.py b/python/tests/test_kontor.py similarity index 100% rename from python/cli/tests/test_kontor.py rename to python/tests/test_kontor.py diff --git a/python/qt/.gitignore b/qt/.gitignore similarity index 100% rename from python/qt/.gitignore rename to qt/.gitignore diff --git a/python/qt/database/__init__.py b/qt/database/__init__.py similarity index 100% rename from python/qt/database/__init__.py rename to qt/database/__init__.py diff --git a/python/qt/database/base.py b/qt/database/base.py similarity index 100% rename from python/qt/database/base.py rename to qt/database/base.py diff --git a/python/qt/database/comic.py b/qt/database/comic.py similarity index 100% rename from python/qt/database/comic.py rename to qt/database/comic.py diff --git a/python/qt/database/media.py b/qt/database/media.py similarity index 100% rename from python/qt/database/media.py rename to qt/database/media.py diff --git a/python/qt/database/metadata.py b/qt/database/metadata.py similarity index 100% rename from python/qt/database/metadata.py rename to qt/database/metadata.py diff --git a/python/cli/kontor/database/tysc.py b/qt/database/tysc.py similarity index 94% rename from python/cli/kontor/database/tysc.py rename to qt/database/tysc.py index 54cacea..733d7ed 100644 --- a/python/cli/kontor/database/tysc.py +++ b/qt/database/tysc.py @@ -28,7 +28,7 @@ class Team(Base): name = Column(String(255), nullable=False, index=True, unique=True) short_name = Column(String(255), nullable=False, ) sport_id = Column(String, ForeignKey("sport.id"), nullable=False) - sport = relationship("Sport", back_populates="positions") + sport = relationship("Sport", back_populates="teams") roosters = relationship("Rooster") @@ -81,7 +81,7 @@ class Rooster(Base): player_id = Column(String, ForeignKey("player.id"), nullable=False, index=True) player = relationship("Player", back_populates="roosters") position_id = Column(String, ForeignKey("field_position.id"), nullable=False, index=True) - position = relationship("roosters") + position = relationship("FieldPosition", back_populates="roosters") cards = relationship("Card") class Vendor(Base): @@ -124,8 +124,8 @@ class Card(Base): card_number = Column(Integer, index=True) year = Column(Integer, index=True) card_set_id = Column(String, ForeignKey("card_set.id"), nullable=False) - card_set = relationship("cards") + card_set = relationship("CardSet", back_populates="cards") rooster_id = Column(String, ForeignKey("rooster.id"), nullable=False) - rooster = relationship("cards") + rooster = relationship("Rooster", back_populates="cards") vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False) vendor = relationship("Vendor", back_populates="cards") diff --git a/qt/gui/__init__.py b/qt/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qt/gui/data_view.py b/qt/gui/data_view.py new file mode 100644 index 0000000..91c66d2 --- /dev/null +++ b/qt/gui/data_view.py @@ -0,0 +1,12 @@ +from abc import ABC, abstractmethod + + +class DataViewMeta(ABC): + @abstractmethod + def get_header(self): + pass + + +class ComicView(DataViewMeta): + def get_header(self): + pass diff --git a/qt/gui/data_view_model.py b/qt/gui/data_view_model.py new file mode 100644 index 0000000..4ffdc8c --- /dev/null +++ b/qt/gui/data_view_model.py @@ -0,0 +1,32 @@ +from typing import List + +from PySide6.QtCore import QModelIndex, QAbstractTableModel +from PySide6.QtGui import Qt + +from gui.data_view import DataViewMeta + + +class DataViewModel(QAbstractTableModel): + def __init__(self): + super().__init__() + self.main_window = None + self._config = None + self._data = List[DataViewMeta] + + def rowCount(self, parent=QModelIndex()): + return len(self._data) + + def columnCount(self, parent=QModelIndex()): + return 0 + + def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole): + return None + + def data(self, index, role=Qt.ItemDataRole.DisplayRole): + return None + + def setData(self, index, value, role=Qt.ItemDataRole.EditRole): + return False + + def flags(self, index): + return None diff --git a/qt/gui/dialogs.py b/qt/gui/dialogs.py new file mode 100644 index 0000000..21dbe92 --- /dev/null +++ b/qt/gui/dialogs.py @@ -0,0 +1,106 @@ +from pathlib import Path + +from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QFileDialog, \ + QCheckBox, QComboBox + + +class ExportKontorDialog(QDialog): + def __init__(self, parent=None, kontor_db=None): + super().__init__(parent) + + self.parent = parent + self.kontor_db = kontor_db + self.file_name = "data.json" + self.tables = [] + self._table_options = {} + + self.export_options = {"JSON": {"ext": ".json"}, "YAML": {"ext": ".yaml"}, "SQLite": {"ext": ".db"}} + self.current_export_type = "JSON" + + buttons = (QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + + self.buttonBox = QDialogButtonBox(buttons) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + layout = QVBoxLayout() + + self.label = QLabel() + self.label.setText("Export DB to data.json") + + self.combo_box = QComboBox() + self.combo_box.addItems(["JSON", "YAML", "SQLite"]) + self.combo_box.currentTextChanged.connect(self.change_export_type) + file_layout = QHBoxLayout() + file_layout.addWidget(self.label) + file_layout.addWidget(self.combo_box) + file_button = QPushButton("Select file") + file_button.clicked.connect(self.select_file) + file_layout.addWidget(file_button) + layout.addLayout(file_layout) + + for table_name in self.kontor_db.get_table_names(): + check_box = QCheckBox(table_name) + check_box.setChecked(True) + self.tables.append(table_name) + self._table_options[table_name] = check_box + check_box.stateChanged.connect(self.change_selection) + layout.addWidget(check_box) + layout.addWidget(self.buttonBox) + self.setLayout(layout) + + def change_selection(self): + self.tables.clear() + for (name, box) in self._table_options.items(): + if box.isChecked(): + self.tables.append(name) + + def change_export_type(self, text): + self.current_export_type = text + self.label.setText(f'Export DB to data.{self.export_options[text]["ext"]}') + + def select_file(self): + file_dialog = QFileDialog() + file_dialog.setFileMode(QFileDialog.FileMode.AnyFile) + file_dialog.setDefaultSuffix(self.export_options[self.current_export_type]["ext"]) + file_dialog.setNameFilter(f'*{self.export_options[self.current_export_type]["ext"]}') + if file_dialog.exec(): + self.file_name = file_dialog.selectedFiles()[0] + export_file = Path(self.file_name) + self.file_name = export_file.with_suffix(self.export_options[self.current_export_type]["ext"]) + self.label.setText(f"Export DB to {self.file_name}") + + def get_tables_to_export(self) -> list: + return self.tables + + +class ImportKontorDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + + self.file_name = None + + QBtn = (QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + + self.buttonBox = QDialogButtonBox(QBtn) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + self.label = QLabel() + self.label.setText("Import DB from data.json") + layout = QVBoxLayout() + file_layout = QHBoxLayout() + file_layout.addWidget(self.label) + file_button = QPushButton("Select file") + file_button.clicked.connect(self.select_file) + file_layout.addWidget(file_button) + layout.addLayout(file_layout) + layout.addWidget(self.buttonBox) + self.setLayout(layout) + + def select_file(self): + file_dialog = QFileDialog() + file_dialog.setFileMode(QFileDialog.FileMode.ExistingFile) + if file_dialog.exec(): + self.file_name = file_dialog.selectedFiles()[0] + self.label.setText(f"Import DB from {self.file_name}") diff --git a/python/qt/gui/main_window.py b/qt/gui/main_window.py similarity index 100% rename from python/qt/gui/main_window.py rename to qt/gui/main_window.py diff --git a/python/qt/gui/model_config.py b/qt/gui/model_config.py similarity index 100% rename from python/qt/gui/model_config.py rename to qt/gui/model_config.py diff --git a/python/qt/gui/table_model.py b/qt/gui/table_model.py similarity index 100% rename from python/qt/gui/table_model.py rename to qt/gui/table_model.py diff --git a/python/qt/kontor.py b/qt/kontor.py similarity index 100% rename from python/qt/kontor.py rename to qt/kontor.py diff --git a/python/qt/pysidedeploy.spec b/qt/pysidedeploy.spec old mode 100755 new mode 100644 similarity index 100% rename from python/qt/pysidedeploy.spec rename to qt/pysidedeploy.spec diff --git a/qt/res/application-export.png b/qt/res/application-export.png new file mode 100644 index 0000000000000000000000000000000000000000..555887a28d64bc812c4dfa98a6ff1da1927b7792 GIT binary patch literal 513 zcmV+c0{;DpP)A0|fCeqz@KJVX8nnt2D9k2UrOajFq_SwR4OUbbUJ;HgeDQF66oE777mBiWdd2F(NKOO zk$Ax17hTui{#6jPXfz7dY87I!*bgRr4TVA=afmd=7~nkYBq$b(su#^>Q?2y;%rJ~= zA;j4sgM^B|5YOb(M4cc`5q!^h_wPg5is0Dq{42l!6{r`}{QE*r00000NkvXXu0mjf Djqud07&@TR)18#v5Hqj8@|Bnpn>z;V`CueWd< z7oI05i2^A#D9Qz9v#hs`;>*D7{eB7>0ppH2SLstJO-8BwR~M`A7kF ztyTlI66tgrtyT-MSPa~yU>3AuKR54&OXJih@;oco-=1sDLQa}`@LC7Iy> O0000dz zNLw#yfl;S$OS+NxP!T0`q27a_KcR=-dJ5{Xm+WEf!J>k|BqE5#C}ImGwQ$$<(wdh& z>AIJ5zVFx9lo8J{@aWY%oIP6CAZKYHol)uRf|554m_YTnpAlGl zL9{5M4)qVt#u>|mLQVrh46q|<)MP|~t8EE-8crSv>~io{4+abW2A~lV0z>OM)mbBD zmWtqV;+?9YGlhpQQ@M9zi{*KRG_T5PQ_nvC9tMo_mT6XEIU5;J+S?d(HD_0f!1ES7 z7@Uj?4!kD>&?r8e&c1jan^9Cs!}3z0d7Izafj)-}rbYZR}EdihS6`M#Fcy= zaw{u#mV;BNWe3{Sqiq;{_M>cOP*8MhP1CLQf3t|C2tpM55ycYh7RL%}Yf+Bp#e!~C z2(WG^ZnlxC>igv!sW2GM8SslVJ(oFwkp~5*6E^0rT$Q zya}^2lB>ITrX0zGEa!lc90$HC4~#^g#%ZB2j3vzHhi;TO$n{?Wwx(^0>^QA_x~H^z zTf=2SxY0rCf|fl@rb7TXYr%pE(0=u@G literal 0 HcmV?d00001 diff --git a/qt/res/cross.png b/qt/res/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..6b9fa6dd36ee8165272a13dd263f573507c78ca6 GIT binary patch literal 544 zcmV+*0^j|KP)L-ku(! z6_?D-?!0+#=VtbVZQGdTnZt~apI_HPK#=zVadHM((E`e*C+RoFb)Qo8-U{Lb7{`Tz zw4B8FZ|o?Wox+p=1wp47C;7ar*Xu~;a?<=sjPv>+otDjJ6FaGt!YnNyxQQhp)G1V! zk;r6ZtyV)c8pTbiRAnHRNXTxti%=+p$4aG2*+mMM&xrdiU^`VPk^N*+HX98^7!HT% zwA%;A{T)GxeQ|RcxyzV%! z$AbYD+)i1R64zB?q}NmTz}5|04~J#YG_goA*Eq(QJvp7pG14hUj1pZ^t>3S*xqHUO ze~r;}zTY_XkZ*}d@gm!;N90h8nBQenBUZ_ukZKNicn*hc_Pmc!Jn|2=s<~}>!Sb>Qm3!QP46b_JFw5YU70Tu$X}=T}ez@@t%j iG9vD)nDux55?}x$+|UyQVK_bj0000tYd4K$mX5uyr2F@fdffp{&DSHtl4|Bn9=ukpCx`#%PTUr-D(@b7;C zhJXJjrnw{;1KBM=6&|E`fsNrGL!Y6%zUh}QUl`(@V)PmQFtotEKmafTHP0uP}7&%m4pcKQ#X)BiAJ2y+PrtBI>9eEIt2-_c7)?*Lsf z5vX5*Af@m-wBJRV%#Fiz=BdPr0!2^a2d-yv7gSU);Z6{`~Oy?E5qSnHUln4*Y%!){F!Y1~5WXoC44g zb7l_)X~q^V6MmWR7e3wj|L|Wb!|^}Y86N$^h+kv_Kw-fP!~#If$6(B4$)LlK#&G6; zC&ShMSAb$5tA9Xg5dOsg@-z^@3;;QS9f&!gG&3|;{Da~@Q2ZB({s%XJ5&#fj0J|In U+D>(nEdT%j07*qoM6N<$g0@&8X#fBK literal 0 HcmV?d00001 From 3a0a0055a630d20e87b4ae97c14eeb0a95b244f8 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Thu, 5 Dec 2024 20:40:13 +0100 Subject: [PATCH 28/91] Add Taskjuggler project file --- kontor.tjp | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 kontor.tjp diff --git a/kontor.tjp b/kontor.tjp new file mode 100644 index 0000000..a5d46c6 --- /dev/null +++ b/kontor.tjp @@ -0,0 +1,30 @@ +project kontor "Kontor" "0.1.0" 2024-12-05 +5m { + timezone "Europe/Berlin" + timeformat "%d.%m.%Y" + numberformat "-" "" "" "," 1 + currencyformat "-" "" "" "," 0 + currency "EUR" + + scenario plan "Plan" { + scenario real "Realität" + } +} + +resource gcpce "Google Cloud Compute Engine" { + efficiency 0.0 + rate 0.25 +} + +task flask "Kontor-Flask" { + task import "Import repository kontor-flask into directory flask" +} +task springboot "Springboot Vaadin" { + task import "Import repository kontor-spring into directory springboot" +} + +taskreport "Arbeitsliste" { + formats html + hidetask ~isleaf() + sorttasks plan.end.up +} + From afb3ac88c8fd6f99e019ef9619d7d3c7eb6a2cb4 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 5 Jan 2025 14:10:15 +0100 Subject: [PATCH 29/91] first implementation to show Comics and MediaFiles --- gui/application-export.png | Bin 0 -> 513 bytes gui/application-import.png | Bin 0 -> 524 bytes gui/arrow-circle-double.png | Bin 0 -> 836 bytes gui/cross.png | Bin 0 -> 544 bytes gui/kontor.py | 2 + gui/media_file_model.py | 91 ++++++++++++++++++++++++++++++++++++ gui/tick.png | Bin 0 -> 634 bytes 7 files changed, 93 insertions(+) create mode 100644 gui/application-export.png create mode 100644 gui/application-import.png create mode 100644 gui/arrow-circle-double.png create mode 100644 gui/cross.png create mode 100644 gui/media_file_model.py create mode 100644 gui/tick.png diff --git a/gui/application-export.png b/gui/application-export.png new file mode 100644 index 0000000000000000000000000000000000000000..555887a28d64bc812c4dfa98a6ff1da1927b7792 GIT binary patch literal 513 zcmV+c0{;DpP)A0|fCeqz@KJVX8nnt2D9k2UrOajFq_SwR4OUbbUJ;HgeDQF66oE777mBiWdd2F(NKOO zk$Ax17hTui{#6jPXfz7dY87I!*bgRr4TVA=afmd=7~nkYBq$b(su#^>Q?2y;%rJ~= zA;j4sgM^B|5YOb(M4cc`5q!^h_wPg5is0Dq{42l!6{r`}{QE*r00000NkvXXu0mjf Djqud07&@TR)18#v5Hqj8@|Bnpn>z;V`CueWd< z7oI05i2^A#D9Qz9v#hs`;>*D7{eB7>0ppH2SLstJO-8BwR~M`A7kF ztyTlI66tgrtyT-MSPa~yU>3AuKR54&OXJih@;oco-=1sDLQa}`@LC7Iy> O0000dz zNLw#yfl;S$OS+NxP!T0`q27a_KcR=-dJ5{Xm+WEf!J>k|BqE5#C}ImGwQ$$<(wdh& z>AIJ5zVFx9lo8J{@aWY%oIP6CAZKYHol)uRf|554m_YTnpAlGl zL9{5M4)qVt#u>|mLQVrh46q|<)MP|~t8EE-8crSv>~io{4+abW2A~lV0z>OM)mbBD zmWtqV;+?9YGlhpQQ@M9zi{*KRG_T5PQ_nvC9tMo_mT6XEIU5;J+S?d(HD_0f!1ES7 z7@Uj?4!kD>&?r8e&c1jan^9Cs!}3z0d7Izafj)-}rbYZR}EdihS6`M#Fcy= zaw{u#mV;BNWe3{Sqiq;{_M>cOP*8MhP1CLQf3t|C2tpM55ycYh7RL%}Yf+Bp#e!~C z2(WG^ZnlxC>igv!sW2GM8SslVJ(oFwkp~5*6E^0rT$Q zya}^2lB>ITrX0zGEa!lc90$HC4~#^g#%ZB2j3vzHhi;TO$n{?Wwx(^0>^QA_x~H^z zTf=2SxY0rCf|fl@rb7TXYr%pE(0=u@G literal 0 HcmV?d00001 diff --git a/gui/cross.png b/gui/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..6b9fa6dd36ee8165272a13dd263f573507c78ca6 GIT binary patch literal 544 zcmV+*0^j|KP)L-ku(! z6_?D-?!0+#=VtbVZQGdTnZt~apI_HPK#=zVadHM((E`e*C+RoFb)Qo8-U{Lb7{`Tz zw4B8FZ|o?Wox+p=1wp47C;7ar*Xu~;a?<=sjPv>+otDjJ6FaGt!YnNyxQQhp)G1V! zk;r6ZtyV)c8pTbiRAnHRNXTxti%=+p$4aG2*+mMM&xrdiU^`VPk^N*+HX98^7!HT% zwA%;A{T)GxeQ|RcxyzV%! z$AbYD+)i1R64zB?q}NmTz}5|04~J#YG_goA*Eq(QJvp7pG14hUj1pZ^t>3S*xqHUO ze~r;}zTY_XkZ*}d@gm!;N90h8nBQenBUZ_ukZKNicn*hc_Pmc!Jn|2=s<~}>!Sb>Qm3!QP46b_JFw5YU70Tu$X}=T}ez@@t%j iG9vD)nDux55?}x$+|UyQVK_bj0000tYd4K$mX5uyr2F@fdffp{&DSHtl4|Bn9=ukpCx`#%PTUr-D(@b7;C zhJXJjrnw{;1KBM=6&|E`fsNrGL!Y6%zUh}QUl`(@V)PmQFtotEKmafTHP0uP}7&%m4pcKQ#X)BiAJ2y+PrtBI>9eEIt2-_c7)?*Lsf z5vX5*Af@m-wBJRV%#Fiz=BdPr0!2^a2d-yv7gSU);Z6{`~Oy?E5qSnHUln4*Y%!){F!Y1~5WXoC44g zb7l_)X~q^V6MmWR7e3wj|L|Wb!|^}Y86N$^h+kv_Kw-fP!~#If$6(B4$)LlK#&G6; zC&ShMSAb$5tA9Xg5dOsg@-z^@3;;QS9f&!gG&3|;{Da~@Q2ZB({s%XJ5&#fj0J|In U+D>(nEdT%j07*qoM6N<$g0@&8X#fBK literal 0 HcmV?d00001 From 245ee2378e4188f25a71fe2734e4246e6e903755 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 5 Jan 2025 14:13:15 +0100 Subject: [PATCH 30/91] display tick and cross for boolean values --- gui/media_file_model.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gui/media_file_model.py b/gui/media_file_model.py index 619ce2e..f06d94f 100644 --- a/gui/media_file_model.py +++ b/gui/media_file_model.py @@ -60,11 +60,11 @@ class MediaFileTableModel(QAbstractTableModel): return value.strftime("%Y-%m-%d %M:%M:%S") if isinstance(value, str): return value -# if isinstance(value, bytes): -# if value == b'\x01': -# return self.main_window.tick -# else: -# return self.main_window.cross + if isinstance(value, bytes): + if value == b'\x01': + return self.main_window.tick + else: + return self.main_window.cross return value if role == Qt.ItemDataRole.DecorationRole: if isinstance(value, bytes): From c8ea7c418867fc1f3ac7ac37d2098c8e5102b1a4 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 5 Jan 2025 21:47:27 +0100 Subject: [PATCH 31/91] refresh table when updated --- gui/application-export.png | Bin 513 -> 0 bytes gui/application-import.png | Bin 524 -> 0 bytes gui/arrow-circle-double.png | Bin 836 -> 0 bytes gui/cross.png | Bin 544 -> 0 bytes gui/kontor.py | 3 +-- gui/media_file_model.py | 36 ++++++++++++++++++++++++++++++------ gui/tick.png | Bin 634 -> 0 bytes 7 files changed, 31 insertions(+), 8 deletions(-) delete mode 100644 gui/application-export.png delete mode 100644 gui/application-import.png delete mode 100644 gui/arrow-circle-double.png delete mode 100644 gui/cross.png delete mode 100644 gui/tick.png diff --git a/gui/application-export.png b/gui/application-export.png deleted file mode 100644 index 555887a28d64bc812c4dfa98a6ff1da1927b7792..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 513 zcmV+c0{;DpP)A0|fCeqz@KJVX8nnt2D9k2UrOajFq_SwR4OUbbUJ;HgeDQF66oE777mBiWdd2F(NKOO zk$Ax17hTui{#6jPXfz7dY87I!*bgRr4TVA=afmd=7~nkYBq$b(su#^>Q?2y;%rJ~= zA;j4sgM^B|5YOb(M4cc`5q!^h_wPg5is0Dq{42l!6{r`}{QE*r00000NkvXXu0mjf Djqud07&@TR)18#v5Hqj8@|Bnpn>z;V`CueWd< z7oI05i2^A#D9Qz9v#hs`;>*D7{eB7>0ppH2SLstJO-8BwR~M`A7kF ztyTlI66tgrtyT-MSPa~yU>3AuKR54&OXJih@;oco-=1sDLQa}`@LC7Iy> O0000dz zNLw#yfl;S$OS+NxP!T0`q27a_KcR=-dJ5{Xm+WEf!J>k|BqE5#C}ImGwQ$$<(wdh& z>AIJ5zVFx9lo8J{@aWY%oIP6CAZKYHol)uRf|554m_YTnpAlGl zL9{5M4)qVt#u>|mLQVrh46q|<)MP|~t8EE-8crSv>~io{4+abW2A~lV0z>OM)mbBD zmWtqV;+?9YGlhpQQ@M9zi{*KRG_T5PQ_nvC9tMo_mT6XEIU5;J+S?d(HD_0f!1ES7 z7@Uj?4!kD>&?r8e&c1jan^9Cs!}3z0d7Izafj)-}rbYZR}EdihS6`M#Fcy= zaw{u#mV;BNWe3{Sqiq;{_M>cOP*8MhP1CLQf3t|C2tpM55ycYh7RL%}Yf+Bp#e!~C z2(WG^ZnlxC>igv!sW2GM8SslVJ(oFwkp~5*6E^0rT$Q zya}^2lB>ITrX0zGEa!lc90$HC4~#^g#%ZB2j3vzHhi;TO$n{?Wwx(^0>^QA_x~H^z zTf=2SxY0rCf|fl@rb7TXYr%pE(0=u@G diff --git a/gui/cross.png b/gui/cross.png deleted file mode 100644 index 6b9fa6dd36ee8165272a13dd263f573507c78ca6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544 zcmV+*0^j|KP)L-ku(! z6_?D-?!0+#=VtbVZQGdTnZt~apI_HPK#=zVadHM((E`e*C+RoFb)Qo8-U{Lb7{`Tz zw4B8FZ|o?Wox+p=1wp47C;7ar*Xu~;a?<=sjPv>+otDjJ6FaGt!YnNyxQQhp)G1V! zk;r6ZtyV)c8pTbiRAnHRNXTxti%=+p$4aG2*+mMM&xrdiU^`VPk^N*+HX98^7!HT% zwA%;A{T)GxeQ|RcxyzV%! z$AbYD+)i1R64zB?q}NmTz}5|04~J#YG_goA*Eq(QJvp7pG14hUj1pZ^t>3S*xqHUO ze~r;}zTY_XkZ*}d@gm!;N90h8nBQenBUZ_ukZKNicn*hc_Pmc!Jn|2=s<~}>!Sb>Qm3!QP46b_JFw5YU70Tu$X}=T}ez@@t%j iG9vD)nDux55?}x$+|UyQVK_bj0000 0: + filter_rule += " AND " + else: + filter_rule += "WHERE " + filter_rule += "review is true" + print(f"{filter_rule=}") + try: + cursor.execute(f"SELECT id, url, review, should_download, file_name, cloud_link FROM media_file {filter_rule}") + except mariadb.Error as error: + print(error) rows = cursor.fetchall() - for row in rows: - data.append(list(row)) - self.status_bar.showMessage(f"{len(rows)} Einträge geladen", 3000) - self._data = data + print(len(rows)) + if len(rows) > 0: + self.beginResetModel() + for row in rows: + data.append(list(row)) + self._data = data + self.endResetModel() + else: + self._data = None + try: + self.layoutChanged.emit() + except: + + self.main_window.statusBar.showMessage(f"{len(rows)} Einträge geladen", 3000) def rowCount(self, parent=QModelIndex()): # The length of the outer list. diff --git a/gui/tick.png b/gui/tick.png deleted file mode 100644 index 2414885b8576481fb6350ed9ce01ab033169fd75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 634 zcmV-=0)_pFP)tYd4K$mX5uyr2F@fdffp{&DSHtl4|Bn9=ukpCx`#%PTUr-D(@b7;C zhJXJjrnw{;1KBM=6&|E`fsNrGL!Y6%zUh}QUl`(@V)PmQFtotEKmafTHP0uP}7&%m4pcKQ#X)BiAJ2y+PrtBI>9eEIt2-_c7)?*Lsf z5vX5*Af@m-wBJRV%#Fiz=BdPr0!2^a2d-yv7gSU);Z6{`~Oy?E5qSnHUln4*Y%!){F!Y1~5WXoC44g zb7l_)X~q^V6MmWR7e3wj|L|Wb!|^}Y86N$^h+kv_Kw-fP!~#If$6(B4$)LlK#&G6; zC&ShMSAb$5tA9Xg5dOsg@-z^@3;;QS9f&!gG&3|;{Da~@Q2ZB({s%XJ5&#fj0J|In U+D>(nEdT%j07*qoM6N<$g0@&8X#fBK From ce1514f20ad0d1f9ff8340f48f97692061cb962e Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 5 Jan 2025 23:44:29 +0100 Subject: [PATCH 32/91] fix errors when displaying empty table --- gui/media_file_model.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gui/media_file_model.py b/gui/media_file_model.py index e34ad26..e5af2a4 100644 --- a/gui/media_file_model.py +++ b/gui/media_file_model.py @@ -35,10 +35,7 @@ class MediaFileTableModel(QAbstractTableModel): filter_rule += "WHERE " filter_rule += "review is true" print(f"{filter_rule=}") - try: - cursor.execute(f"SELECT id, url, review, should_download, file_name, cloud_link FROM media_file {filter_rule}") - except mariadb.Error as error: - print(error) + cursor.execute(f"SELECT id, url, review, should_download, file_name, cloud_link FROM media_file {filter_rule}") rows = cursor.fetchall() print(len(rows)) if len(rows) > 0: @@ -49,14 +46,13 @@ class MediaFileTableModel(QAbstractTableModel): self.endResetModel() else: self._data = None - try: - self.layoutChanged.emit() - except: - + self.layoutChanged.emit() self.main_window.statusBar.showMessage(f"{len(rows)} Einträge geladen", 3000) def rowCount(self, parent=QModelIndex()): # The length of the outer list. + if self._data is None: + return 0 return len(self._data) def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): @@ -78,6 +74,8 @@ class MediaFileTableModel(QAbstractTableModel): return str(col+1) def data(self, index, role=Qt.ItemDataRole.DisplayRole): + if self._data is None: + return None value = self._data[index.row()][index.column()] if role == Qt.ItemDataRole.DisplayRole: if isinstance(value, datetime): @@ -101,6 +99,8 @@ class MediaFileTableModel(QAbstractTableModel): def columnCount(self, index=QModelIndex()): # The following takes the first sub-list, and returns # the length (only works if all rows are an equal length) + if self._data is None: + return 5 return len(self._data[0]) def setData(self, index, value, role=Qt.ItemDataRole.EditRole): From d6410e25846a5ae8fab844280b8f7138c558199b Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 6 Jan 2025 17:07:20 +0100 Subject: [PATCH 33/91] implement generic table model implement generic table model which reads table info from db and constructs table view --- gui/kontor.py | 2 +- gui/media_file_model.py | 115 ---------------------------------------- 2 files changed, 1 insertion(+), 116 deletions(-) delete mode 100644 gui/media_file_model.py diff --git a/gui/kontor.py b/gui/kontor.py index fcdeb82..59e770c 100644 --- a/gui/kontor.py +++ b/gui/kontor.py @@ -6,7 +6,7 @@ from pathlib import Path import yaml from PySide6.QtGui import QAction, QIcon -from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView, QHBoxLayout, QCheckBox +from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView from PySide6.QtWidgets import QApplication, QLabel, QMainWindow from platformdirs import PlatformDirs diff --git a/gui/media_file_model.py b/gui/media_file_model.py deleted file mode 100644 index e5af2a4..0000000 --- a/gui/media_file_model.py +++ /dev/null @@ -1,115 +0,0 @@ -from datetime import datetime - -import mariadb -from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt -from PySide6.QtGui import QColor - - -class MediaFileTableModel(QAbstractTableModel): - - def __init__(self, db_config, main_window): - super().__init__() - self.main_window = main_window - self._data = [] - self.mariadb_conn = mariadb.connect( - host=db_config['mariadb']['host'], - port=db_config['mariadb']['port'], - user=db_config['mariadb']['user'], - password=db_config['mariadb']['password'], - database=db_config['mariadb']['database'] - ) - self.refresh() - - def refresh(self): - data = [] - cursor = self.mariadb_conn.cursor() - filter_rule = "" - print(self.main_window.filter["download"].isChecked()) - if self.main_window.filter["download"].isChecked(): - print(self.main_window.filter["download"].isChecked()) - filter_rule = "WHERE should_download is true" - if self.main_window.filter["review"].isChecked(): - if len(filter_rule) > 0: - filter_rule += " AND " - else: - filter_rule += "WHERE " - filter_rule += "review is true" - print(f"{filter_rule=}") - cursor.execute(f"SELECT id, url, review, should_download, file_name, cloud_link FROM media_file {filter_rule}") - rows = cursor.fetchall() - print(len(rows)) - if len(rows) > 0: - self.beginResetModel() - for row in rows: - data.append(list(row)) - self._data = data - self.endResetModel() - else: - self._data = None - self.layoutChanged.emit() - self.main_window.statusBar.showMessage(f"{len(rows)} Einträge geladen", 3000) - - def rowCount(self, parent=QModelIndex()): - # The length of the outer list. - if self._data is None: - return 0 - return len(self._data) - - def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): - if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: - match col: - case 0: - return "ID" - case 1: - return "URL" - case 2: - return "Review" - case 3: - return "Download" - case 4: - return "Filename" - case 5: - return "Cloud Link" - if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: - return str(col+1) - - def data(self, index, role=Qt.ItemDataRole.DisplayRole): - if self._data is None: - return None - value = self._data[index.row()][index.column()] - if role == Qt.ItemDataRole.DisplayRole: - if isinstance(value, datetime): - return value.strftime("%Y-%m-%d %M:%M:%S") - if isinstance(value, str): - return value - if isinstance(value, bytes): - if value == b'\x01': - return self.main_window.tick - else: - return self.main_window.cross - return value - if role == Qt.ItemDataRole.DecorationRole: - if isinstance(value, bytes): - # print('{}: {}'.format(value, type(value))) - if value == b'\x01': - return self.main_window.tick - else: - return self.main_window.cross - - def columnCount(self, index=QModelIndex()): - # The following takes the first sub-list, and returns - # the length (only works if all rows are an equal length) - if self._data is None: - return 5 - return len(self._data[0]) - - def setData(self, index, value, role=Qt.ItemDataRole.EditRole): - if role == Qt.ItemDataRole.EditRole: - self._data[index.row()][index.column()] = value - if role == Qt.ItemDataRole.CheckStateRole: - checked = value == Qt.CheckState.Checked - self._data[index.row()][index.column()] = checked - return True - - def flags(self, index): - return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate From 0d2f27f7718a1a31a0dd49cb336526fa3ab6f2cc Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Wed, 8 Jan 2025 22:31:20 +0100 Subject: [PATCH 34/91] import from kontor-flask --- flask/config.py | 47 ++ flask/docs/Makefile | 20 + flask/docs/conf.py | 200 +++++++++ flask/docs/index.rst | 20 + flask/kontor/__init__.py | 74 ++++ flask/kontor/admin/__init__.py | 4 + flask/kontor/admin/forms.py | 17 + flask/kontor/admin/views.py | 63 +++ flask/kontor/auth/__init__.py | 4 + flask/kontor/auth/forms.py | 50 +++ flask/kontor/auth/views.py | 79 ++++ flask/kontor/comics/__init__.py | 3 + flask/kontor/comics/forms.py | 31 ++ flask/kontor/comics/models.py | 51 +++ flask/kontor/comics/views.py | 222 ++++++++++ flask/kontor/home/__init__.py | 3 + flask/kontor/home/views.py | 39 ++ flask/kontor/library/__init__.py | 3 + flask/kontor/library/forms.py | 35 ++ flask/kontor/library/models.py | 55 +++ flask/kontor/library/views.py | 228 ++++++++++ flask/kontor/models.py | 66 +++ flask/kontor/office/__init__.py | 3 + flask/kontor/static/css/style.css | 126 ++++++ flask/kontor/templates/admin/users/user.html | 21 + flask/kontor/templates/admin/users/users.html | 58 +++ flask/kontor/templates/auth/login.html | 18 + flask/kontor/templates/auth/register.html | 14 + flask/kontor/templates/base.html | 107 +++++ flask/kontor/templates/comics/artists.html | 58 +++ flask/kontor/templates/comics/comics.html | 66 +++ flask/kontor/templates/comics/publishers.html | 58 +++ .../templates/home/admin_dashboard.html | 31 ++ flask/kontor/templates/home/dashboard.html | 20 + flask/kontor/templates/home/index.html | 20 + flask/kontor/templates/library/authors.html | 58 +++ flask/kontor/templates/library/books.html | 74 ++++ .../kontor/templates/library/publishers.html | 58 +++ flask/kontor/templates/simpleform.html | 21 + .../kontor/templates/tysc/manufacturers.html | 58 +++ flask/kontor/templates/tysc/players.html | 60 +++ flask/kontor/templates/tysc/positions.html | 62 +++ flask/kontor/templates/tysc/sports.html | 58 +++ flask/kontor/templates/tysc/teams.html | 60 +++ flask/kontor/tysc/__init__.py | 377 ++++++++++++++++ flask/kontor/tysc/forms.py | 76 ++++ flask/kontor/tysc/models.py | 155 +++++++ flask/kontor/tysc/views.py | 405 ++++++++++++++++++ flask/kontor/version.py | 2 + flask/pylint.cfg | 382 +++++++++++++++++ flask/requirements.txt | 11 + flask/tests/__init__.py | 38 ++ flask/tests/test_comics_model.py | 51 +++ flask/tests/test_comics_view.py | 44 ++ flask/tests/test_config.txt | 4 + flask/tests/test_kontor_model.py | 14 + flask/tests/test_kontor_view.py | 58 +++ flask/tests/test_library_model.py | 49 +++ flask/tests/test_library_view.py | 44 ++ flask/tests/test_tysc_model.py | 53 +++ flask/tests/test_tysc_view.py | 110 +++++ 61 files changed, 4296 insertions(+) create mode 100644 flask/config.py create mode 100644 flask/docs/Makefile create mode 100644 flask/docs/conf.py create mode 100644 flask/docs/index.rst create mode 100644 flask/kontor/__init__.py create mode 100644 flask/kontor/admin/__init__.py create mode 100644 flask/kontor/admin/forms.py create mode 100644 flask/kontor/admin/views.py create mode 100644 flask/kontor/auth/__init__.py create mode 100644 flask/kontor/auth/forms.py create mode 100644 flask/kontor/auth/views.py create mode 100644 flask/kontor/comics/__init__.py create mode 100644 flask/kontor/comics/forms.py create mode 100644 flask/kontor/comics/models.py create mode 100644 flask/kontor/comics/views.py create mode 100644 flask/kontor/home/__init__.py create mode 100644 flask/kontor/home/views.py create mode 100644 flask/kontor/library/__init__.py create mode 100644 flask/kontor/library/forms.py create mode 100644 flask/kontor/library/models.py create mode 100644 flask/kontor/library/views.py create mode 100644 flask/kontor/models.py create mode 100644 flask/kontor/office/__init__.py create mode 100644 flask/kontor/static/css/style.css create mode 100644 flask/kontor/templates/admin/users/user.html create mode 100644 flask/kontor/templates/admin/users/users.html create mode 100644 flask/kontor/templates/auth/login.html create mode 100644 flask/kontor/templates/auth/register.html create mode 100644 flask/kontor/templates/base.html create mode 100644 flask/kontor/templates/comics/artists.html create mode 100644 flask/kontor/templates/comics/comics.html create mode 100644 flask/kontor/templates/comics/publishers.html create mode 100644 flask/kontor/templates/home/admin_dashboard.html create mode 100644 flask/kontor/templates/home/dashboard.html create mode 100644 flask/kontor/templates/home/index.html create mode 100644 flask/kontor/templates/library/authors.html create mode 100644 flask/kontor/templates/library/books.html create mode 100644 flask/kontor/templates/library/publishers.html create mode 100644 flask/kontor/templates/simpleform.html create mode 100644 flask/kontor/templates/tysc/manufacturers.html create mode 100644 flask/kontor/templates/tysc/players.html create mode 100644 flask/kontor/templates/tysc/positions.html create mode 100644 flask/kontor/templates/tysc/sports.html create mode 100644 flask/kontor/templates/tysc/teams.html create mode 100644 flask/kontor/tysc/__init__.py create mode 100644 flask/kontor/tysc/forms.py create mode 100644 flask/kontor/tysc/models.py create mode 100644 flask/kontor/tysc/views.py create mode 100644 flask/kontor/version.py create mode 100644 flask/pylint.cfg create mode 100644 flask/requirements.txt create mode 100644 flask/tests/__init__.py create mode 100644 flask/tests/test_comics_model.py create mode 100644 flask/tests/test_comics_view.py create mode 100644 flask/tests/test_config.txt create mode 100644 flask/tests/test_kontor_model.py create mode 100644 flask/tests/test_kontor_view.py create mode 100644 flask/tests/test_library_model.py create mode 100644 flask/tests/test_library_view.py create mode 100644 flask/tests/test_tysc_model.py create mode 100644 flask/tests/test_tysc_view.py diff --git a/flask/config.py b/flask/config.py new file mode 100644 index 0000000..4ea5e78 --- /dev/null +++ b/flask/config.py @@ -0,0 +1,47 @@ +# config.py + + +class Config(object): + """ + Common configurations + """ + + # Put any configurations here that are common across all environments + host = '127.0.0.1' + port = 8500 + database = 'kontor' + + +class DevelopmentConfig(Config): + """ + Development configurations + """ + + DEBUG = True + host = '0.0.0.0' + database = 'kontor_dev' + + +class ProductionConfig(Config): + """ + Production configurations + """ + + DEBUG = False + +class TestingConfig(Config): + """ + Testing configurations + """ + + TESTING = True + DEBUG = True + port = 8600 + database = 'kontor_test' + +app_config = { + 'development': DevelopmentConfig, + 'production': ProductionConfig, + 'testing': TestingConfig +} + diff --git a/flask/docs/Makefile b/flask/docs/Makefile new file mode 100644 index 0000000..d7218b4 --- /dev/null +++ b/flask/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = KontorFlask +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/flask/docs/conf.py b/flask/docs/conf.py new file mode 100644 index 0000000..7278f2d --- /dev/null +++ b/flask/docs/conf.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Kontor Flask documentation build configuration file, created by +# sphinx-quickstart on Mon Nov 6 08:53:04 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.mathjax', + 'sphinx.ext.ifconfig', + 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'Kontor Flask' +copyright = '2017, Thomas Peetz' +author = 'Thomas Peetz' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.1' +# The full version, including alpha/beta/rc tags. +release = '0.0.7' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'de' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# This is required for the alabaster theme +# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars +html_sidebars = { + '**': [ + 'relations.html', # needs 'show_related': True theme option to display + 'searchbox.html', + ] +} + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'KontorFlaskdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'KontorFlask.tex', 'Kontor Flask Documentation', + 'Thomas Peetz', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'kontorflask', 'Kontor Flask Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'KontorFlask', 'Kontor Flask Documentation', + author, 'KontorFlask', 'One line description of project.', + 'Miscellaneous'), +] + + + +# -- Options for Epub output ---------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project +epub_author = author +epub_publisher = author +epub_copyright = copyright + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'https://docs.python.org/': None} diff --git a/flask/docs/index.rst b/flask/docs/index.rst new file mode 100644 index 0000000..ca4df98 --- /dev/null +++ b/flask/docs/index.rst @@ -0,0 +1,20 @@ +.. Kontor Flask documentation master file, created by + sphinx-quickstart on Mon Nov 6 08:53:04 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Kontor Flask's documentation! +======================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/flask/kontor/__init__.py b/flask/kontor/__init__.py new file mode 100644 index 0000000..961e132 --- /dev/null +++ b/flask/kontor/__init__.py @@ -0,0 +1,74 @@ +""" +Module kontor implements Kontor application. +""" + +from flask import Flask +from flask_bootstrap import Bootstrap +from flask_login import LoginManager +from pymodm import connect + +# local imports +from config import app_config +from .version import __version__ + + +_LOGIN_MANAGER_ = LoginManager() + + +def get_host(config_name): + """ + Returns host address from configuration. + :param config_name: + :return: host address + """ + host = app_config[config_name].host + return host + + +def get_port(config_name): + """ + Returns port number from configuration. + :param config_name: + :return: port number + """ + port = app_config[config_name].port + return port + + +def create_app(config_name): + """ + Returns Flask application. + :param config_name: + :return: Flask application + """ + app = Flask(__name__, instance_relative_config=True) + app.config.from_object(app_config[config_name]) + app.config.from_pyfile('config.py') + + database = "mongodb://localhost:27017/{}".format(app_config[config_name].database) + connect(database, alias="kontor") + + Bootstrap(app) + _LOGIN_MANAGER_.init_app(app) + _LOGIN_MANAGER_.login_message = "You must be logged in to access this page." + _LOGIN_MANAGER_.login_view = "auth.login" + + from .admin.views import ADMIN as ADMIN_BLUEPRINT + app.register_blueprint(ADMIN_BLUEPRINT, url_prefix='/admin') + + from .auth.views import AUTH as AUTH_BLUEPRINT + app.register_blueprint(AUTH_BLUEPRINT) + + from .home.views import HOME as HOME_BLUEPRINT + app.register_blueprint(HOME_BLUEPRINT) + + from .comics.views import COMIC as COMIC_BLUEPRINT + app.register_blueprint(COMIC_BLUEPRINT, url_prefix='/comics') + + from .library.views import LIBRARY as LIBRARY_BLUEPRINT + app.register_blueprint(LIBRARY_BLUEPRINT, url_prefix='/library') + + from .tysc.views import TYSC as TYSC_BLUEPRINT + app.register_blueprint(TYSC_BLUEPRINT, url_prefix='/tysc') + + return app diff --git a/flask/kontor/admin/__init__.py b/flask/kontor/admin/__init__.py new file mode 100644 index 0000000..ca00a4b --- /dev/null +++ b/flask/kontor/admin/__init__.py @@ -0,0 +1,4 @@ +""" +Module admin implements administration functions. +""" +from . import views diff --git a/flask/kontor/admin/forms.py b/flask/kontor/admin/forms.py new file mode 100644 index 0000000..d05cfaf --- /dev/null +++ b/flask/kontor/admin/forms.py @@ -0,0 +1,17 @@ +""" +Define form to edit users +""" +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField +from wtforms.validators import DataRequired + + +class UserEditForm(FlaskForm): + """ + Form for admin to edit users + """ + email = StringField('Email', validators=[DataRequired()]) + username = StringField('Username', validators=[DataRequired()]) + first_name = StringField('First Name', validators=[DataRequired()]) + last_name = StringField('Last Name', validators=[DataRequired()]) + submit = SubmitField('Submit') diff --git a/flask/kontor/admin/views.py b/flask/kontor/admin/views.py new file mode 100644 index 0000000..e96d8f0 --- /dev/null +++ b/flask/kontor/admin/views.py @@ -0,0 +1,63 @@ +""" +Define routing rules for managing users +""" +from flask import abort, Blueprint, flash, redirect, render_template, url_for +from flask_login import current_user, login_required + +from .forms import UserEditForm +from ..models import User + +ADMIN = Blueprint('admin', __name__) + + +def check_admin(): + """ + Prevent non-admins from accessing the page + """ + if not current_user.is_admin: + abort(403) + + +# User Views +@ADMIN.route('/users') +@login_required +def list_users(): + """ + List all users + """ + check_admin() + + users = User.objects.all() + return render_template('admin/users/users.html', + users=users, title='Users') + + +@ADMIN.route('/users/edit/', methods=['GET', 'POST']) +@login_required +def edit_user(user_id): + """ + Edit an user. + """ + check_admin() + + user = User.objects.get({'username': user_id}) + + # prevent admin from being assigned a department or role + if user.is_admin: + abort(403) + + form = UserEditForm(obj=user) + if form.validate_on_submit(): + user.email = form.email.data + user.username = form.username.data + user.first_name = form.first_name.data + user.last_name = form.last_name.data + user.save() + flash('You have successfully edited an user.') + + # redirect to the roles page + return redirect(url_for('admin.list_users')) + + return render_template('admin/users/user.html', + user=user, form=form, + title='Edit User') diff --git a/flask/kontor/auth/__init__.py b/flask/kontor/auth/__init__.py new file mode 100644 index 0000000..2d9b1ca --- /dev/null +++ b/flask/kontor/auth/__init__.py @@ -0,0 +1,4 @@ +""" +Module auth implements authentication functions. +""" +from . import views diff --git a/flask/kontor/auth/forms.py b/flask/kontor/auth/forms.py new file mode 100644 index 0000000..6e4d6dd --- /dev/null +++ b/flask/kontor/auth/forms.py @@ -0,0 +1,50 @@ +""" +Contains forms for authentication. +""" +from flask_wtf import FlaskForm +from wtforms import PasswordField, StringField, SubmitField, ValidationError +from wtforms.validators import DataRequired, Email, EqualTo + +from ..models import User + + +class RegistrationForm(FlaskForm): + """ + Form for users to create new account + """ + email = StringField('Email', validators=[DataRequired(), Email()]) + username = StringField('Username', validators=[DataRequired()]) + first_name = StringField('First Name', validators=[DataRequired()]) + last_name = StringField('Last Name', validators=[DataRequired()]) + password = PasswordField('Password', validators=[DataRequired(), + EqualTo('confirm_password') + ]) + confirm_password = PasswordField('Confirm Password') + submit = SubmitField('Register') + + def validate_email(self, field): + """ + Check if email is already in use. + :param field: + :return: + """ + if User.objects.raw({'email': field.data}).count() > 0: + raise ValidationError('Email is already in use.') + + def validate_username(self, field): + """ + check if username is already in use. + :param field: + :return: + """ + if User.objects.raw({'username': field.data}).count() > 0: + raise ValidationError('Username is already in use.') + + +class LoginForm(FlaskForm): + """ + Form for users to login + """ + email = StringField('Email', validators=[DataRequired(), Email()]) + password = PasswordField('Password', validators=[DataRequired()]) + submit = SubmitField('Login') diff --git a/flask/kontor/auth/views.py b/flask/kontor/auth/views.py new file mode 100644 index 0000000..217ae18 --- /dev/null +++ b/flask/kontor/auth/views.py @@ -0,0 +1,79 @@ +""" +Define routing rules for registering users and login and logout. +""" +from flask import Blueprint, flash, redirect, render_template, url_for +from flask_login import login_required, login_user, logout_user +from .forms import LoginForm, RegistrationForm +from ..models import User + +AUTH = Blueprint('auth', __name__) + + +@AUTH.route('/register', methods=['GET', 'POST']) +def register(): + """ + Handle requests to the /register route + Add an employee to the database through the registration form + """ + form = RegistrationForm() + if form.validate_on_submit(): + user = User() + user.email = form.email.data + user.username = form.username.data + user.first_name = form.first_name.data + user.last_name = form.last_name.data + user.password = form.password.data + # add employee to the database + user.save() + flash('You have successfully registered! You may now login.') + + # redirect to the login page + return redirect(url_for('auth.login')) + + # load registration template + return render_template('auth/register.html', form=form, title='Register') + + +@AUTH.route('/login', methods=['GET', 'POST']) +def login(): + """ + Handle requests to the /login route + Validate given email with matching password + :return: + """ + form = LoginForm() + if form.validate_on_submit(): + + # check whether employee exists in the database and whether + # the password entered matches the password in the database + user = User.objects.get({'email': form.email.data}) + if user is not None and user.verify_password( + form.password.data): + # log employee in + login_user(user) + + # redirect to the appropriate dashboard page + if user.is_admin: + return redirect(url_for('home.admin_dashboard')) + else: + return redirect(url_for('home.dashboard')) + + # when login details are incorrect + else: + flash('Invalid email or password.') + + # load login template + return render_template('auth/login.html', form=form, title='Login') + +@AUTH.route('/logout') +@login_required +def logout(): + """ + Handle requests to the /logout route + Log an employee out through the logout link + """ + logout_user() + flash('You have successfully been logged out.') + + # redirect to the login page + return redirect(url_for('auth.login')) diff --git a/flask/kontor/comics/__init__.py b/flask/kontor/comics/__init__.py new file mode 100644 index 0000000..e947d82 --- /dev/null +++ b/flask/kontor/comics/__init__.py @@ -0,0 +1,3 @@ +""" +Define routing rules for comic related information +""" diff --git a/flask/kontor/comics/forms.py b/flask/kontor/comics/forms.py new file mode 100644 index 0000000..0bae45f --- /dev/null +++ b/flask/kontor/comics/forms.py @@ -0,0 +1,31 @@ +"""Define form to edit publisher, artists and comics""" +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, BooleanField, SelectField +from wtforms.validators import DataRequired +from bson import ObjectId + + +class PublisherForm(FlaskForm): + """ + Form to add and edit a Comic publisher + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class ArtistForm(FlaskForm): + """ + Form to add and edit a Comic publisher + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class ComicForm(FlaskForm): + """ + Form to add and edit Comics + """ + title = StringField('Title', validators=[DataRequired()]) + publisher = SelectField('Publisher', coerce=ObjectId) + current_order = BooleanField('Current Order') + submit = SubmitField('Submit') diff --git a/flask/kontor/comics/models.py b/flask/kontor/comics/models.py new file mode 100644 index 0000000..e39e744 --- /dev/null +++ b/flask/kontor/comics/models.py @@ -0,0 +1,51 @@ +"""This modules declares the model for Comic related information.""" + +from flask import current_app +from pymongo.write_concern import WriteConcern +from pymodm import MongoModel, fields + + +class Publisher(MongoModel): + """Class Publisher represents a publisher of a comic.""" + name = fields.CharField() + + def __str__(self): + return "Publisher({})".format(self.name) + + @property + def comics(self): + """ + Return list of comics which has reference to this publisher + :return: + """ + comics = Comic.objects.raw({'publisher': self.pk}) + current_app.logger.debug(comics) + return comics + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Artist(MongoModel): + """Class Artist represents a comic artist.""" + name = fields.CharField() + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Comic(MongoModel): + """Class Comic represents a comic.""" + title = fields.CharField() + publisher = fields.ReferenceField(Publisher) + current_order = fields.BooleanField(default=False) + completed = fields.BooleanField(default=False) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) diff --git a/flask/kontor/comics/views.py b/flask/kontor/comics/views.py new file mode 100644 index 0000000..090b032 --- /dev/null +++ b/flask/kontor/comics/views.py @@ -0,0 +1,222 @@ +""" +Define routing rules for comics, publisher and artists +""" +from flask import Blueprint, flash, redirect, render_template, url_for +from flask_login import login_required +from bson import ObjectId +from pymongo.errors import PyMongoError +from .forms import ComicForm, PublisherForm, ArtistForm +from .models import Comic, Publisher, Artist + + +COMIC = Blueprint('comic', __name__) + + +@COMIC.route('/artists') +@login_required +def list_artists(): + """ + List all artists + :return: + """ + artists = Artist.objects.all() + return render_template('comics/artists.html', + artists=artists, title="Artists") + + +@COMIC.route('/artists/edit/', methods=['GET', 'POST']) +@login_required +def edit_artist(artist_id): + """ + Edit a comic artist + """ + artist = Artist.objects.get({'_id': ObjectId(artist_id)}) + form = ArtistForm(obj=artist) + if form.validate_on_submit(): + artist.name = form.name.data + artist.save() + flash('You have successfully edited the artist.') + return redirect(url_for('comic.list_artists')) + form.name.data = artist.name + return render_template('simpleform.html', action="Edit", + form=form, title="Edit Artist") + + +@COMIC.route('/artists/add', methods=['GET', 'POST']) +@login_required +def add_artist(): + """ + Add a artist + :return: + """ + form = ArtistForm() + if form.validate_on_submit(): + artist = Artist() + artist.name = form.name.data + try: + # add publisher to the database + artist.save() + flash('You have successfully added a new artist.') + except PyMongoError: + # in case publisher name already exists + flash('Error: artist name already exists.') + return redirect(url_for('comic.list_artists')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Artist") + + +@COMIC.route('/artists/delete/', methods=['GET', 'POST']) +@login_required +def delete_artist(artist_id): + """ + Delete a comic artist + :param artist_id: + :return: + """ + artist = Artist.objects.raw({'_id': ObjectId(artist_id)}) + if artist: + artist.delete() + flash('You have successfully deleted the comic artist.') + return redirect(url_for('comic.list_artists')) + + +@COMIC.route('/publishers') +@login_required +def list_publishers(): + """ + List all publishers + :return: + """ + publishers = Publisher.objects.all() + return render_template('comics/publishers.html', + publishers=publishers, title="Publishers") + + +@COMIC.route('/publishers/add', methods=['GET', 'POST']) +@login_required +def add_publisher(): + """ + Add a publisher to the database + :return: + """ + form = PublisherForm() + if form.validate_on_submit(): + publisher = Publisher() + publisher.name = form.name.data + try: + # add publisher to the database + publisher.save() + flash('You have successfully added a new publisher.') + except PyMongoError: + # in case publisher name already exists + flash('Error: publisher name already exists.') + return redirect(url_for('comic.list_publishers')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Publisher") + + +@COMIC.route('/publishers/edit/', methods=['GET', 'POST']) +@login_required +def edit_publisher(publisher_id): + """ + Edit a publisher + """ + publisher = Publisher.objects.get({'_id': ObjectId(publisher_id)}) + form = PublisherForm(obj=publisher) + if form.validate_on_submit(): + publisher.name = form.name.data + publisher.save() + flash('You have successfully edited the publisher.') + return redirect(url_for('comic.list_publishers')) + form.name.data = publisher.name + return render_template('simpleform.html', action="Edit", + form=form, title="Edit Publisher") + + +@COMIC.route('/publishers/delete/', methods=['GET', 'POST']) +@login_required +def delete_publisher(publisher_id): + """ + Delete a publisher + :param publisher_id: ObjectId of publisher + :return: + """ + publisher = Publisher.objects.raw({'_id': ObjectId(publisher_id)}) + if publisher: + publisher.delete() + flash('You have successfully deleted the publisher.') + return redirect(url_for('comic.list_publishers')) + + +@COMIC.route('/comics') +@login_required +def list_comics(): + """ + List all comics + :return: + """ + comics = Comic.objects.all() + return render_template('comics/comics.html', + comics=comics, title="Comics") + + +@COMIC.route('/comics/edit/', methods=['GET', 'POST']) +@login_required +def edit_comic(comic_id): + """ + Edit a comic + """ + comic = Comic.objects.get({'_id': ObjectId(comic_id)}) + form = ComicForm(obj=comic) + form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] + form.publisher.default = comic.publisher.pk + form.publisher.process_data(comic.publisher.pk) + if form.validate_on_submit(): + comic.title = form.title.data + comic.current_order = form.current_order.data + comic.publisher = form.publisher.data + comic.save() + flash('You have successfully edited the comic.') + return redirect(url_for('comic.list_comics')) + form.title.data = comic.title + form.current_order.data = comic.current_order + return render_template('simpleform.html', action="Edit", + form=form, title="Edit Comic") + + +@COMIC.route('/comics/add', methods=['GET', 'POST']) +@login_required +def add_comic(): + """ + Add a comic + :return: + """ + form = ComicForm() + form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] + if form.validate_on_submit(): + comic = Comic() + comic.title = form.title.data + comic.publisher = form.publisher.data + try: + comic.save() + flash('You have successfully added a new comic.') + except PyMongoError: + flash('Error: comic title already exists.') + return redirect(url_for('comic.list_comics')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Comic") + + +@COMIC.route('/comics/delete/', methods=['GET', 'POST']) +@login_required +def delete_comic(comic_id): + """ + Delete a comic + :param comic_id: + :return: + """ + comic = Comic.objects.raw({'_id': ObjectId(comic_id)}) + if comic: + comic.delete() + flash('You have successfully deleted the comic.') + return redirect(url_for('comic.list_comics')) diff --git a/flask/kontor/home/__init__.py b/flask/kontor/home/__init__.py new file mode 100644 index 0000000..d4468a9 --- /dev/null +++ b/flask/kontor/home/__init__.py @@ -0,0 +1,3 @@ +""" +Module for the Kontor homepage. +""" diff --git a/flask/kontor/home/views.py b/flask/kontor/home/views.py new file mode 100644 index 0000000..4e4d539 --- /dev/null +++ b/flask/kontor/home/views.py @@ -0,0 +1,39 @@ +""" +Define routing rules for homepage and dashboards +""" + +from flask import Blueprint, render_template, abort +from flask_login import login_required, current_user + +HOME = Blueprint('home', __name__) + + +@HOME.route('/') +def homepage(): + """ + Render the homepage template on the / route + """ + return render_template('home/index.html', title="Welcome") + + +@HOME.route('/dashboard') +@login_required +def dashboard(): + """ + Render the dashboard template on the /dashboard route + """ + return render_template('home/dashboard.html', title="Dashboard") + + +@HOME.route('/admin/dashboard') +@login_required +def admin_dashboard(): + """ + Render the admin_dashboard template on the /admin/dashboard route + :return: + """ + # prevent non-admins from accessing the page + if not current_user.is_admin: + abort(403) + + return render_template('home/admin_dashboard.html', title="Dashboard") diff --git a/flask/kontor/library/__init__.py b/flask/kontor/library/__init__.py new file mode 100644 index 0000000..31eb0bc --- /dev/null +++ b/flask/kontor/library/__init__.py @@ -0,0 +1,3 @@ +""" +Define routing rules for library related information +""" diff --git a/flask/kontor/library/forms.py b/flask/kontor/library/forms.py new file mode 100644 index 0000000..a080781 --- /dev/null +++ b/flask/kontor/library/forms.py @@ -0,0 +1,35 @@ +""" +Define form to edit publisher, artists and books +""" +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, SelectField, IntegerField +from wtforms.validators import DataRequired, Optional, NumberRange +from bson import ObjectId + + +class PublisherForm(FlaskForm): + """ + Form to add and edit a Comic publisher + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class AuthorForm(FlaskForm): + """ + Form to add and edit a Comic publisher + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class BookForm(FlaskForm): + """ + Form to add and edit Comics + """ + title = StringField('Title', validators=[DataRequired()]) + author = SelectField('Author', coerce=ObjectId) + publisher = SelectField('Publisher', coerce=ObjectId) + isbn = StringField('ISBN', validators=[Optional()]) + year = IntegerField('Year', validators=[NumberRange(min=1970, max=2050)]) + submit = SubmitField('Submit') diff --git a/flask/kontor/library/models.py b/flask/kontor/library/models.py new file mode 100644 index 0000000..7ca5c58 --- /dev/null +++ b/flask/kontor/library/models.py @@ -0,0 +1,55 @@ +""" +This module declares the model for the library related information. +""" +from pymongo.write_concern import WriteConcern +from pymodm import MongoModel, fields + + +class Publisher(MongoModel): + """ + Class Publisher represents a publisher of a book. + """ + name = fields.CharField() + + def __str__(self): + return "Publisher({})".format(self.name) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Author(MongoModel): + """ + Class Author represents an author of a book. + """ + name = fields.CharField() + + def __str__(self): + return "Author({})".format(self.name) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Book(MongoModel): + """ + Class book represents a book. + """ + title = fields.CharField() + author = fields.ReferenceField(Author) + publisher = fields.ReferenceField(Publisher) + isbn = fields.CharField(blank=True) + year = fields.IntegerField(blank=True) + edition = fields.CharField() + + def __str__(self): + return "Book({})".format(self.title) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) diff --git a/flask/kontor/library/views.py b/flask/kontor/library/views.py new file mode 100644 index 0000000..1b53eee --- /dev/null +++ b/flask/kontor/library/views.py @@ -0,0 +1,228 @@ +"""Define routing rules for book publishers""" +from flask import Blueprint, flash, redirect, render_template, url_for +from flask_login import login_required +from bson import ObjectId +from pymongo.errors import PyMongoError +from .forms import PublisherForm, AuthorForm, BookForm +from .models import Publisher, Author, Book + + +LIBRARY = Blueprint('library', __name__) + + +@LIBRARY.route('/publishers') +@login_required +def list_publishers(): + """ + List all publishers + :return: + """ + publishers = Publisher.objects.all() + return render_template('library/publishers.html', + publishers=publishers, title="Publishers") + + +@LIBRARY.route('/publishers/add', methods=['GET', 'POST']) +@login_required +def add_publisher(): + """ + Add a publisher to the database + :return: + """ + form = PublisherForm() + if form.validate_on_submit(): + publisher = Publisher() + publisher.name = form.name.data + try: + publisher.save() + flash('You have successfully added a new publisher.') + except PyMongoError: + flash('Error: publisher name already exists.') + return redirect(url_for('library.list_publishers')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Publisher") + + +@LIBRARY.route('/publishers/edit/', methods=['GET', 'POST']) +@login_required +def edit_publisher(publisher_id): + """ + Edit a publisher + """ + publisher = Publisher.objects.get({'_id': ObjectId(publisher_id)}) + form = PublisherForm(obj=publisher) + if form.validate_on_submit(): + publisher.name = form.name.data + publisher.save() + flash('You have successfully edited the publisher.') + return redirect(url_for('library.list_publishers')) + form.name.data = publisher.name + return render_template('simpleform.html', action="Edit", + form=form, publisher=publisher, title="Edit Publisher") + + +@LIBRARY.route('/publishers/delete/', methods=['GET', 'POST']) +@login_required +def delete_publisher(publisher_id): + """ + Delete a publisher + :param publisher_id: ObjectId of publisher + :return: + """ + publisher = Publisher.objects.raw({'_id': ObjectId(publisher_id)}) + if publisher: + publisher.delete() + flash('You have successfully deleted the publisher.') + return redirect(url_for('library.list_publishers')) + + +@LIBRARY.route('/authors') +@login_required +def list_authors(): + """ + List all authors + :return: + """ + authors = Author.objects.all() + return render_template('library/authors.html', + authors=authors, title="Authors") + + +@LIBRARY.route('/authors/edit/', methods=['GET', 'POST']) +@login_required +def edit_author(author_id): + """ + Edit a book artist + """ + author = Author.objects.get({'_id': ObjectId(author_id)}) + form = AuthorForm(obj=author) + if form.validate_on_submit(): + author.name = form.name.data + author.save() + flash('You have successfully edited the author.') + return redirect(url_for('library.list_authors')) + form.name.data = author.name + return render_template('simpleform.html', action="Edit", + form=form, author=author, title="Edit Author") + + +@LIBRARY.route('/authors/add', methods=['GET', 'POST']) +@login_required +def add_author(): + """ + Add a author + :return: + """ + form = AuthorForm() + if form.validate_on_submit(): + author = Author() + author.name = form.name.data + try: + author.save() + flash('You have successfully added a new author.') + except PyMongoError: + flash('Error: author name already exists.') + return redirect(url_for('library.list_authors')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Author") + + +@LIBRARY.route('/authors/delete/', methods=['GET', 'POST']) +@login_required +def delete_author(author_id): + """ + Delete a author + :param author_id: + :return: + """ + author = Author.objects.raw({'_id': ObjectId(author_id)}) + if author: + author.delete() + flash('You have successfully deleted the author.') + return redirect(url_for('library.list_authors')) + + +@LIBRARY.route('/books') +@login_required +def list_books(): + """ + List all comics + :return: + """ + books = Book.objects.all() + return render_template('library/books.html', + books=books, title="Books") + + +@LIBRARY.route('/books/edit/', methods=['GET', 'POST']) +@login_required +def edit_book(book_id): + """ + Edit a book + """ + book = Book.objects.get({'_id': ObjectId(book_id)}) + form = BookForm(obj=book) + form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] + if book.publisher: + form.publisher.default = book.publisher.pk + form.publisher.process_data(book.publisher.pk) + form.author.choices = [(a.pk, a.name) for a in Author.objects.all()] + if book.author: + form.author.default = book.author.pk + form.author.process_data(book.author.pk) + if form.validate_on_submit(): + book.title = form.title.data + book.publisher = form.publisher.data + book.author = form.author.data + if form.isbn.data: + book.isbn = form.isbn.data + else: + book.isbn = None + book.year = form.year.data + book.save() + flash('You have successfully edited the book.') + return redirect(url_for('library.list_books')) + form.title.data = book.title + return render_template('simpleform.html', action="Edit", + form=form, book=book, title="Edit Book") + + +@LIBRARY.route('/books/add', methods=['GET', 'POST']) +@login_required +def add_book(): + """ + Add a book + :return: + """ + form = BookForm() + form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] + form.author.choices = [(a.pk, a.name) for a in Author.objects.all()] + if form.validate_on_submit(): + book = Book() + book.title = form.title.data + book.publisher = form.publisher.data + book.author = form.author.data + book.year = form.year.data + try: + book.save() + flash('You have successfully added a new book.') + except PyMongoError: + flash('Error: book title already exists.') + return redirect(url_for('library.list_books')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Book") + + +@LIBRARY.route('/books/delete/', methods=['GET', 'POST']) +@login_required +def delete_book(book_id): + """ + Delete a book + :param book_id: + :return: + """ + book = Book.objects.raw({'_id': ObjectId(book_id)}) + if book: + book.delete() + flash('You have successfully deleted the book.') + return redirect(url_for('library.list_books')) diff --git a/flask/kontor/models.py b/flask/kontor/models.py new file mode 100644 index 0000000..7150364 --- /dev/null +++ b/flask/kontor/models.py @@ -0,0 +1,66 @@ +""" +This modules declares the model for Kontor users. +""" +from pymodm import MongoModel, fields +from pymongo.write_concern import WriteConcern +from flask_login import UserMixin +from werkzeug.security import generate_password_hash, check_password_hash +from kontor import _LOGIN_MANAGER_ + + +class User(UserMixin, MongoModel): + """ + This class represents an user for the Kontor application. + """ + email = fields.EmailField() + username = fields.CharField(max_length=60) + first_name = fields.CharField(max_length=60) + last_name = fields.CharField(max_length=60) + password_hash = fields.CharField(max_length=128) + is_admin = fields.BooleanField(default=False) + + @property + def password(self): + """ + Prevent pasword from being accessed + """ + raise AttributeError('password is not a readable attribute.') + + @password.setter + def password(self, password): + """ + Set password to a hashed password + """ + self.password_hash = generate_password_hash(password) + + def verify_password(self, password): + """ + Check if hashed password matches actual password + """ + return check_password_hash(self.password_hash, password) + + def get_id(self): + """ + Get username as id + :return: + """ + return self.username + + def __repr__(self): + return ''.format(self.username) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +# Set up user_loader +@_LOGIN_MANAGER_.user_loader +def load_user(user_name): + """ + Get list of users from database + :param user_name: + :return: + """ + return User.objects.get({'username': user_name}) diff --git a/flask/kontor/office/__init__.py b/flask/kontor/office/__init__.py new file mode 100644 index 0000000..d830d41 --- /dev/null +++ b/flask/kontor/office/__init__.py @@ -0,0 +1,3 @@ +""" +Define routing rules for office related information +""" diff --git a/flask/kontor/static/css/style.css b/flask/kontor/static/css/style.css new file mode 100644 index 0000000..35fe053 --- /dev/null +++ b/flask/kontor/static/css/style.css @@ -0,0 +1,126 @@ +/* app/static/css/style.css */ + +body, html { + width: 100%; + height: 100%; +} + +body, h1, h2, h3 { + font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 700; +} + +a, .navbar-default .navbar-brand, .navbar-default .navbar-nav>li>a { + color: #aec251; +} + +a:hover, .navbar-default .navbar-brand:hover, .navbar-default .navbar-nav>li>a:hover { + color: #687430; +} + +footer { + padding: 50px 0; + background-color: #f8f8f8; +} + +p.copyright { + margin: 15px 0 0; +} + +.alert-info { + width: 50%; + margin: auto; + color: #687430; + background-color: #e6ecca; + border-color: #aec251; +} + +.btn-default { + border-color: #aec251; + color: #aec251; +} + +.btn-default:hover { + background-color: #aec251; +} + +.center { + margin: auto; + width: 70%; + padding: 10px; +} + +.content-section { + padding: 50px 0; + border-top: 1px solid #e7e7e7; +} + +.footer, .push { + clear: both; + height: 4em; +} + +.intro-divider { + width: 400px; + border-top: 1px solid #f8f8f8; + border-bottom: 1px solid rgba(0,0,0,0.2); +} + +.intro-header { + padding-top: 50px; + padding-bottom: 50px; + text-align: center; + color: #f8f8f8; + background: url(../img/intro-bg.jpg) no-repeat center center; + background-size: cover; + height: 100%; +} + +.intro-message { + position: relative; + padding-top: 20%; + padding-bottom: 20%; +} + +.intro-message > h1 { + margin: 0; + text-shadow: 2px 2px 3px rgba(0,0,0,0.6); + font-size: 5em; +} + +.intro-message > h3 { + text-shadow: 2px 2px 3px rgba(0,0,0,0.6); +} + +.lead { + font-size: 18px; + font-weight: 400; +} + +.topnav { + font-size: 14px; +} + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -4em; +} + +.outer { + display: table; + position: absolute; + height: 70%; + width: 100%; +} + +.middle { + display: table-cell; + vertical-align: middle; +} + +.inner { + margin-left: auto; + margin-right: auto; +} diff --git a/flask/kontor/templates/admin/users/user.html b/flask/kontor/templates/admin/users/user.html new file mode 100644 index 0000000..50df2c4 --- /dev/null +++ b/flask/kontor/templates/admin/users/user.html @@ -0,0 +1,21 @@ + + +{% import "bootstrap/wtf.html" as wtf %} +{% extends "base.html" %} +{% block title %}Edit User{% endblock %} +{% block body %} +
+
+
+
+
+

Edit User

+
+
+ {{ wtf.quick_form(form) }} +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/admin/users/users.html b/flask/kontor/templates/admin/users/users.html new file mode 100644 index 0000000..b23f67c --- /dev/null +++ b/flask/kontor/templates/admin/users/users.html @@ -0,0 +1,58 @@ + + +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Users{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Users

+ {% if users %} +
+
+ + + + + + + + + + + {% for user in users %} + {% if user.is_admin %} + + + + + + + {% else %} + + + + + + + {% endif %} + {% endfor %} + +
Name Email Username Edit
Admin N/A N/A N/A
{{ user.first_name }} {{ user.last_name }} {{ user.email }} {{ user.username }} + + Edit + +
+
+ {% endif %} +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/auth/login.html b/flask/kontor/templates/auth/login.html new file mode 100644 index 0000000..8b70e01 --- /dev/null +++ b/flask/kontor/templates/auth/login.html @@ -0,0 +1,18 @@ + + +{% import "bootstrap/utils.html" as utils %} +{% import "bootstrap/wtf.html" as wtf %} +{% extends "base.html" %} +{% block title %}Login{% endblock %} +{% block body %} +
+
+ {{ utils.flashed_messages() }} +
+
+

Login to your account

+
+ {{ wtf.quick_form(form) }} +
+
+{% endblock %} diff --git a/flask/kontor/templates/auth/register.html b/flask/kontor/templates/auth/register.html new file mode 100644 index 0000000..55d4918 --- /dev/null +++ b/flask/kontor/templates/auth/register.html @@ -0,0 +1,14 @@ + + +{% import "bootstrap/wtf.html" as wtf %} +{% extends "base.html" %} +{% block title %}Register{% endblock %} +{% block body %} +
+
+

Register for an account

+
+ {{ wtf.quick_form(form) }} +
+
+{% endblock %} diff --git a/flask/kontor/templates/base.html b/flask/kontor/templates/base.html new file mode 100644 index 0000000..80bdc27 --- /dev/null +++ b/flask/kontor/templates/base.html @@ -0,0 +1,107 @@ + + + + + {{ title }} | Kontor + + + + + + + + +
+ {% block body %} + {% endblock %} +
+
+
+
+
+

+

+ +
+
+
+ + + diff --git a/flask/kontor/templates/comics/artists.html b/flask/kontor/templates/comics/artists.html new file mode 100644 index 0000000..a92415e --- /dev/null +++ b/flask/kontor/templates/comics/artists.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Comic Artists{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Comic Artists

+ {% if artists %} +
+
+ + + + + + + + + + {% for artist in artists %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ artist.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No artists have been added.

+
+ {% endif %} + + + Add Artist + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/comics/comics.html b/flask/kontor/templates/comics/comics.html new file mode 100644 index 0000000..c596ad5 --- /dev/null +++ b/flask/kontor/templates/comics/comics.html @@ -0,0 +1,66 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Comics{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Comics

+ {% if comics %} +
+
+ + + + + + + + + + + + {% for comic in comics %} + + + + + + + + {% endfor %} + +
Title Publisher Current Order Edit Delete
{{ comic.title }} + + {{ comic.publisher.name }} + + {{ comic.current_order }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No comics have been added.

+
+ {% endif %} + + + Add Comic + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/comics/publishers.html b/flask/kontor/templates/comics/publishers.html new file mode 100644 index 0000000..aa09f7b --- /dev/null +++ b/flask/kontor/templates/comics/publishers.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Comic Publishers{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Comic Publishers

+ {% if publishers %} +
+
+ + + + + + + + + + {% for publisher in publishers %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ publisher.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No comics have been added.

+
+ {% endif %} + + + Add Publisher + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/home/admin_dashboard.html b/flask/kontor/templates/home/admin_dashboard.html new file mode 100644 index 0000000..77a0c99 --- /dev/null +++ b/flask/kontor/templates/home/admin_dashboard.html @@ -0,0 +1,31 @@ + + +{% extends "base.html" %} +{% block title %}Admin Dashboard{% endblock %} +{% block body %} +
+
+
+
+
+

Admin Dashboard

+
+ +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/home/dashboard.html b/flask/kontor/templates/home/dashboard.html new file mode 100644 index 0000000..6764375 --- /dev/null +++ b/flask/kontor/templates/home/dashboard.html @@ -0,0 +1,20 @@ + + +{% extends "base.html" %} +{% block title %}Dashboard{% endblock %} +{% block body %} +
+
+
+
+
+

The Dashboard

+

We made it here!

+
+ +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/home/index.html b/flask/kontor/templates/home/index.html new file mode 100644 index 0000000..5b01225 --- /dev/null +++ b/flask/kontor/templates/home/index.html @@ -0,0 +1,20 @@ + + +{% extends "base.html" %} +{% block title %}Home{% endblock %} +{% block body %} +
+
+
+
+
+

Project Kontor

+

Store everything!

+
+ +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/library/authors.html b/flask/kontor/templates/library/authors.html new file mode 100644 index 0000000..8c24f7b --- /dev/null +++ b/flask/kontor/templates/library/authors.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Library Authors{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Library Authors

+ {% if authors %} +
+
+ + + + + + + + + + {% for author in authors %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ author.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No artists have been added.

+
+ {% endif %} + + + Add Author + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/library/books.html b/flask/kontor/templates/library/books.html new file mode 100644 index 0000000..6a6ef76 --- /dev/null +++ b/flask/kontor/templates/library/books.html @@ -0,0 +1,74 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Books{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Books

+ {% if books %} +
+
+ + + + + + + + + + + + + + {% for book in books %} + + + + + + + + + + {% endfor %} + +
Title Author Publisher ISBN Year Edit Delete
{{ book.title }} + + {{ book.author.name }} + + + + {{ book.publisher.name }} + + {{ book.isbn }} {{ book.year }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No books have been added.

+
+ {% endif %} + + + Add Book + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/library/publishers.html b/flask/kontor/templates/library/publishers.html new file mode 100644 index 0000000..d0d363c --- /dev/null +++ b/flask/kontor/templates/library/publishers.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Book Publishers{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Book Publishers

+ {% if publishers %} +
+
+ + + + + + + + + + {% for publisher in publishers %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ publisher.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No publishers have been added.

+
+ {% endif %} + + + Add Publisher + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/simpleform.html b/flask/kontor/templates/simpleform.html new file mode 100644 index 0000000..6ce9d3b --- /dev/null +++ b/flask/kontor/templates/simpleform.html @@ -0,0 +1,21 @@ +{% import "bootstrap/wtf.html" as wtf %} +{% extends "base.html" %} +{% block title %}{{ title }}{% endblock %} +{% block body %} +
+
+
+
+
+

{{ title }}

+
+
+ {{ wtf.quick_form(form) }} +
+
+
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/tysc/manufacturers.html b/flask/kontor/templates/tysc/manufacturers.html new file mode 100644 index 0000000..f71ed75 --- /dev/null +++ b/flask/kontor/templates/tysc/manufacturers.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}{{ title }}{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

{{ title }}

+ {% if manufacturers %} +
+
+ + + + + + + + + + {% for manufacturer in manufacturers %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ manufacturer.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No manufacturers have been added.

+
+ {% endif %} + + + Add Manufacturer + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/tysc/players.html b/flask/kontor/templates/tysc/players.html new file mode 100644 index 0000000..db25067 --- /dev/null +++ b/flask/kontor/templates/tysc/players.html @@ -0,0 +1,60 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}{{ title }}{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

{{ title }}

+ {% if players %} +
+
+ + + + + + + + + + + {% for player in players %} + + + + + + + {% endfor %} + +
First Name Last Name Edit Delete
{{ player.first_name }} {{ player.last_name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No players have been added.

+
+ {% endif %} + + + Add Player + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/tysc/positions.html b/flask/kontor/templates/tysc/positions.html new file mode 100644 index 0000000..3b49e6e --- /dev/null +++ b/flask/kontor/templates/tysc/positions.html @@ -0,0 +1,62 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}{{ title }}{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

{{ title }}

+ {% if positions %} +
+
+ + + + + + + + + + + + {% for position in positions %} + + + + + + + + {% endfor %} + +
Name Description Sport Edit Delete
{{ position.name }} {{ position.description }} {{ position.sport.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No positions have been added.

+
+ {% endif %} + + + Add Position + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/tysc/sports.html b/flask/kontor/templates/tysc/sports.html new file mode 100644 index 0000000..1efe09a --- /dev/null +++ b/flask/kontor/templates/tysc/sports.html @@ -0,0 +1,58 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Sports{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Sports

+ {% if sports %} +
+
+ + + + + + + + + + {% for sport in sports %} + + + + + + {% endfor %} + +
Name Edit Delete
{{ sport.name }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No sports have been added.

+
+ {% endif %} + + + Add Sport + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/templates/tysc/teams.html b/flask/kontor/templates/tysc/teams.html new file mode 100644 index 0000000..c51e9bf --- /dev/null +++ b/flask/kontor/templates/tysc/teams.html @@ -0,0 +1,60 @@ +{% import "bootstrap/utils.html" as utils %} +{% extends "base.html" %} +{% block title %}Teams{% endblock %} +{% block body %} +
+
+
+
+
+ {{ utils.flashed_messages() }} +
+

Teams

+ {% if teams %} +
+
+ + + + + + + + + + + {% for team in teams %} + + + + + + + {% endfor %} + +
Name Shortname Edit Delete
{{ team.name }} {{ team.shortname }} + + Edit + + + + Delete + +
+
+
+ {% else %} +
+

No teams have been added.

+
+ {% endif %} + + + Add Team + +
+
+
+
+
+{% endblock %} diff --git a/flask/kontor/tysc/__init__.py b/flask/kontor/tysc/__init__.py new file mode 100644 index 0000000..9f06f83 --- /dev/null +++ b/flask/kontor/tysc/__init__.py @@ -0,0 +1,377 @@ +""" +Define routing rules for TradeYourSportsCards related information +""" +from .models import Sport, Team, Position, Player +from .models import Manufacturer, CardSet, ParallelSet, InsertSet, Card + + +def initialize_model(): + """ + Initialize collections for module TradeYourSportsCards. + """ + initialize_sport() + initialize_position() + initialize_teams() + initialize_players() + initialize_manufacturers() + initialize_card_sets() + initialize_parallel_sets() + initialize_insert_sets() + initialize_cards() + + +def initialize_sport(): + """ + Initialize collection Sport with american sports. + """ + for sport in Sport.objects.all(): + sport.delete() + sports = ["Baseball", "Basketball", "Football", "Hockey"] + for sport_name in sports: + sport = Sport() + sport.name = sport_name + sport.save() + + +def initialize_position(): + """ + Initialize collection Position with data. + """ + for position in Position.objects.all(): + position.delete() + positions = { + 'Quarterback': {'short': 'QB', 'sport': 'Football'}, + 'Wide Receiver': {'short': 'WR', 'sport': 'Football'}, + 'Runningback': {'short': 'RB', 'sport': 'Football'}, + 'Linebacker': {'short': 'LB', 'sport': 'Football'}, + 'Tight End': {'short': 'TE', 'sport': 'Football'}, + 'Fullback': {'short': 'FB', 'sport': 'Football'}, + 'Strong Safety': {'short': 'SS', 'sport': 'Football'}, + 'Defensive End': {'short': 'DE', 'sport': 'Football'}, + 'Kicker': {'short': 'K', 'sport': 'Football'}, + 'Punter': {'short': 'P', 'sport': 'Football'}, + 'Left Guard': {'short': 'LG', 'sport': 'Football'}, + 'Right Guard': {'short': 'RG', 'sport': 'Football'}, + 'Offensive Tackle': {'short': 'OT', 'sport': 'Football'}, + 'Defensive Back': {'short': 'DB', 'sport': 'Football'}, + 'Cornerback': {'short': 'CB', 'sport': 'Football'}, + 'Catcher': {'short': 'C', 'sport': 'Baseball'}, + 'First Base': {'short': '1B', 'sport': 'Baseball'}, + 'Second Base': {'short': '2B', 'sport': 'Baseball'}, + 'Third Base': {'short': '3B', 'sport': 'Baseball'}, + 'Shortstop': {'short': 'SS', 'sport': 'Baseball'}, + 'Left Field': {'short': 'LF', 'sport': 'Baseball'}, + 'Center Field': {'short': 'CF', 'sport': 'Baseball'}, + 'Right Field': {'short': 'RF', 'sport': 'Baseball'}, + 'Designated Hitter': {'short': 'DH', 'sport': 'Baseball'}, + 'Pitcher': {'short': 'P', 'sport': 'Baseball'} + } + for key in positions: + sport = Sport.objects.get({'name': positions.get(key)['sport']}) + position = Position() + position.description = key + position.name = positions.get(key)['short'] + position.sport = sport + position.save() + + +def initialize_teams(): + """ + Initialize collection Team with data. + """ + for team in Team.objects.all(): + team.delete() + teams = {'Buffalo Bills': {'short': 'Bills', 'sport': 'Football'}, + 'Indianapolis Colts': {'short': 'Colts', 'sport': 'Football'}, + 'Miami Dolphins': {'short': 'Dolphins', 'sport': 'Football'}, + 'New England Patriots': {'short': 'Patriots', 'sport': 'Football'}, + 'New York Jets': {'short': 'Jets', 'sport': 'Football'}, + 'Baltimore Ravens': {'short': 'Ravens', 'sport': 'Football'}, + 'Cincinnati Bengals': {'short': 'Bengals', 'sport': 'Football'}, + 'Cleveland Browns': {'short': 'Browns', 'sport': 'Football'}, + 'Jacksonville Jaguars': {'short': 'Jaguars', 'sport': 'Football'}, + 'Pittsburgh Steelers': {'short': 'Steelers', 'sport': 'Football'}, + 'Tennessee Titans': {'short': 'Titans', 'sport': 'Football'}, + 'Denver Broncos': {'short': 'Broncos', 'sport': 'Football'}, + 'Kansas City Chiefs': {'short': 'Chiefs', 'sport': 'Football'}, + 'Oakland Raiders': {'short': 'Raiders', 'sport': 'Football'}, + 'San Diego Chargers': {'short': 'Chargers', 'sport': 'Football'}, + 'Seattle Seahawks': {'short': 'Seahawks', 'sport': 'Football'}, + 'Arizona Cardinals': {'short': 'Cardinals', 'sport': 'Football'}, + 'Dallas Cowboys': {'short': 'Cowboys', 'sport': 'Football'}, + 'New York Giants': {'short': 'Giants', 'sport': 'Football'}, + 'Philadelphia Eagles': {'short': 'Eagles', 'sport': 'Football'}, + 'Washington Redskins': {'short': 'Redskins', 'sport': 'Football'}, + 'Chicago Bears': {'short': 'Bears', 'sport': 'Football'}, + 'Detroit Lions': {'short': 'Lions', 'sport': 'Football'}, + 'Green Bay Packers': {'short': 'Packers', 'sport': 'Football'}, + 'Minnesota Vikings': {'short': 'Vikings', 'sport': 'Football'}, + 'Tampa Bay Buccaneers': {'short': 'Buccaneers', 'sport': 'Football'}, + 'Atlanta Falcons': {'short': 'Falcons', 'sport': 'Football'}, + 'Carolina Panthers': {'short': 'Panthers', 'sport': 'Football'}, + 'New Orleans Saints': {'short': 'Saints', 'sport': 'Football'}, + 'St.Louis Rams': {'short': 'Rams', 'sport': 'Football'}, + 'San Francisco 49ers': {'short': '49ers', 'sport': 'Football'}, + 'Baltimore Orioles': {'short': 'Orioles', 'sport': 'Baseball'}, + 'Boston Red Sox': {'short': 'Red Sox', 'sport': 'Baseball'}, + 'New York Yankees': {'short': 'Yankees', 'sport': 'Baseball'}, + 'Tampa Bay Devil Rays': {'short': 'Devil Rays', 'sport': 'Baseball'}, + 'Toronto Blue Jays': {'short': 'Blue Jays', 'sport': 'Baseball'}, + 'Chicago White Sox': {'short': 'White Sox', 'sport': 'Baseball'}, + 'Cleveland Indians': {'short': 'Indians', 'sport': 'Baseball'}, + 'Detroit Tigers': {'short': 'Tigers', 'sport': 'Baseball'}, + 'Kansas City Royals': {'short': 'Royals', 'sport': 'Baseball'}, + 'Minnesota Twins': {'short': 'Twins', 'sport': 'Baseball'}, + 'Anaheim Angels': {'short': 'Angels', 'sport': 'Baseball'}, + 'Oakland Athletics': {'short': 'Athletics', 'sport': 'Baseball'}, + 'Seattle Mariners': {'short': 'Mariners', 'sport': 'Baseball'}, + 'Texas Rangers': {'short': 'Rangers', 'sport': 'Baseball'}, + 'Atlanta Braves': {'short': 'Braves', 'sport': 'Baseball'}, + 'Florida Marlins': {'short': 'Marlins', 'sport': 'Baseball'}, + 'Montreal Expos': {'short': 'Expos', 'sport': 'Baseball'}, + 'New York Mets': {'short': 'Mets', 'sport': 'Baseball'}, + 'Philadelphia Phillies': {'short': 'Phillies', 'sport': 'Baseball'}, + 'Chicago Cubs': {'short': 'Cubs', 'sport': 'Baseball'}, + 'Cincinnati Reds': {'short': 'Reds', 'sport': 'Baseball'}, + 'Houston Astros': {'short': 'Astros', 'sport': 'Baseball'}, + 'Milwaukee Brewers': {'short': 'Brewers', 'sport': 'Baseball'}, + 'Pittsburgh Pirates': {'short': 'Pirates', 'sport': 'Baseball'}, + 'St.Louis Cardinals': {'short': 'Cardinals', 'sport': 'Baseball'}, + 'Arizona Diamondbacks': {'short': 'Diamondbacks', 'sport': 'Baseball'}, + 'Colorado Rockies': {'short': 'Rockies', 'sport': 'Baseball'}, + 'Los Angeles Dodgers': {'short': 'Dodgers', 'sport': 'Baseball'}, + 'San Diego Padres': {'short': 'Padres', 'sport': 'Baseball'}, + 'San Francisco Giants': {'short': 'Giants', 'sport': 'Baseball'}, + 'Boston Celtics': {'short': 'Celtics', 'sport': 'Basketball'}, + 'Miami Heat': {'short': 'Heat', 'sport': 'Basketball'}, + 'New Jersey Nets': {'short': 'Mets', 'sport': 'Basketball'}, + 'New York Knicks': {'short': 'Knicks', 'sport': 'Basketball'}, + 'Orlando Magic': {'short': 'Magic', 'sport': 'Basketball'}, + 'Philadelphia 76ers': {'short': '76ers', 'sport': 'Basketball'}, + 'Washington Wizards': {'short': 'Wizards', 'sport': 'Basketball'}, + 'Atlanta Hawks': {'short': 'Hawks', 'sport': 'Basketball'}, + 'Charlotte Hornets': {'short': 'Hornets', 'sport': 'Basketball'}, + 'Chicago Bulls': {'short': 'Bulls', 'sport': 'Basketball'}, + 'Cleveland Cavaliers': {'short': 'Cavaliers', 'sport': 'Basketball'}, + 'Detroit Pistons': {'short': 'Pistons', 'sport': 'Basketball'}, + 'Indiana Pacers': {'short': 'Pacers', 'sport': 'Basketball'}, + 'Milwaukee Bucks': {'short': 'Bucks', 'sport': 'Basketball'}, + 'Toronto Raptors': {'short': 'Raptors', 'sport': 'Basketball'}, + 'Dallas Mavericks': {'short': 'Mavericks', 'sport': 'Basketball'}, + 'Denver Nuggets': {'short': 'Nuggets', 'sport': 'Basketball'}, + 'Houston Rockets': {'short': 'Rockets', 'sport': 'Basketball'}, + 'Minnesota Timberwolves': {'short': 'Timberwolves', 'sport': 'Basketball'}, + 'San Antonio Spurs': {'short': 'Spurs', 'sport': 'Basketball'}, + 'Utah Jazz': {'short': 'Jazz', 'sport': 'Basketball'}, + 'Vancouver Grizzlies': {'short': 'Grizzlies', 'sport': 'Basketball'}, + 'Golden State Warriors': {'short': 'Warriors', 'sport': 'Basketball'}, + 'Los Angeles Clippers': {'short': 'Clippers', 'sport': 'Basketball'}, + 'Los Angeles Lakers': {'short': 'Lakers', 'sport': 'Basketball'}, + 'Phoenix Suns': {'short': 'Suns', 'sport': 'Basketball'}, + 'Portland Trail Blazers': {'short': 'Blazers', 'sport': 'Basketball'}, + 'Sacramento Kings': {'short': 'Kings', 'sport': 'Basketball'}, + 'Seattle SuperSonics': {'short': 'SuperSonics', 'sport': 'Basketball'}, + 'Boston Bruins': {'short': 'Bruins', 'sport': 'Hockey'}, + 'Buffalo Sabres': {'short': 'Sabres', 'sport': 'Hockey'}, + 'Montreal Canadiens': {'short': 'Canadiens', 'sport': 'Hockey'}, + 'Ottawa Senators': {'short': 'Senators', 'sport': 'Hockey'}, + 'Toronto Maple Leafs': {'short': 'Maple Leafs', 'sport': 'Hockey'}, + 'New Jersey Devils': {'short': 'Devils', 'sport': 'Hockey'}, + 'New York Islander': {'short': 'Islander', 'sport': 'Hockey'}, + 'New York Rangers': {'short': 'Rangers', 'sport': 'Hockey'}, + 'Philadelphia Flyers': {'short': 'Flyers', 'sport': 'Hockey'}, + 'Pittsburgh Penguins': {'short': 'Penguins', 'sport': 'Hockey'}, + 'Atlanta Trashers': {'short': 'Trashers', 'sport': 'Hockey'}, + 'Carolina Hurricanes': {'short': 'Hurricanes', 'sport': 'Hockey'}, + 'Florida Panthers': {'short': 'Panthers', 'sport': 'Hockey'}, + 'Tampa Bay Lightnings': {'short': 'Lightnings', 'sport': 'Hockey'}, + 'Washington Capitals': {'short': 'Capitals', 'sport': 'Hockey'}, + 'Chicago Blackhawks': {'short': 'Blackhawks', 'sport': 'Hockey'}, + 'Columbo Blue Jackets': {'short': 'Blue Jackets', 'sport': 'Hockey'}, + 'Detroit Red Wings': {'short': 'Red Wings', 'sport': 'Hockey'}, + 'Nashville Predators': {'short': 'Predators', 'sport': 'Hockey'}, + 'St.Louis Blues': {'short': 'Blues', 'sport': 'Hockey'}, + 'Calgary Flames': {'short': 'Flames', 'sport': 'Hockey'}, + 'Colorado Avalanche': {'short': 'Avalanche', 'sport': 'Hockey'}, + 'Edmonton Oilers': {'short': 'Oilers', 'sport': 'Hockey'}, + 'Minnesota Wild': {'short': 'Wild', 'sport': 'Hockey'}, + 'Vancouver Canucks': {'short': 'Canucks', 'sport': 'Hockey'}, + 'Anaheim Mighty Ducks': {'short': 'Mighty Ducks', 'sport': 'Hockey'}, + 'Dallas Stars': {'short': 'Stars', 'sport': 'Hockey'}, + 'Los Angeles Kings': {'short': 'Kings', 'sport': 'Hockey'}, + 'Phoenix Coyotes': {'short': 'Coyotes', 'sport': 'Hockey'}, + 'San Jose Sharks': {'short': 'Sharks', 'sport': 'Hockey'}, + 'Houston Texans': {'short': 'Texans', 'sport': 'Football'}, + 'Houston Oilers': {'short': 'Oilers', 'sport': 'Football'} + } + for key in teams: + sport = Sport.objects.get({'name': teams.get(key)['sport']}) + team = Team() + team.name = key + team.shortname = teams.get(key)['short'] + team.sport = sport + team.save() + + +def initialize_players(): + """ + Initialize collection Manufacturer with data. + """ + for player in Player.objects.all(): + player.delete() + players = ['Pathon, Jerome', 'Bruschi, Tedy', 'Couch, Tim', 'Shea, Aaron', + 'Lewis, Jamal', 'Lewis, Jermaine', 'Banks, Tony', 'Fuamatu-Ma\'Afala, Chris', + 'Bettis, Jerome', 'Stewart, Kordell', 'Moon, Warren', 'Lockett, Kevin', + 'Gannon, Rich', 'Jett, James', 'Strong, Mack', 'Huard, Brock', + 'Watters, Ricky', 'Aikman, Troy', 'LaFleur, David', 'Brazzell, Chris', + 'Dayne, Ron', 'Brown, Na', 'Small, Torrance', 'Lewis, Chad', 'Murrell, Adrian', + 'Smith, Maurice', 'Chandler, Chris', 'Kanell, Danny', 'Williams, Ricky', + 'Garcia, Jeff', 'Streets, Tai', 'Garner, Charlie', 'Rice, Jerry', + 'Owens, Terrell', 'Bruce, Isaac', 'Canidate, Trung'] + for player_name in players: + player = Player() + (last_name, first_name) = player_name.split(',') + player.last_name = last_name.strip() + player.first_name = first_name.strip() + player.save() + + +def initialize_manufacturers(): + """ + Initialize collection Manufacturer with data. + """ + for manufacturer in Manufacturer.objects.all(): + manufacturer.delete() + manufacturers = ['Pacific', 'Fleer', 'Bowman', 'Topps', 'Donruss', + 'Score', 'Flair', 'Upper Deck'] + for manufacturer_name in manufacturers: + manufacturer = Manufacturer() + manufacturer.name = manufacturer_name + manufacturer.save() + + +def initialize_card_sets(): + """ + Initialize collection CardSet with data. + """ + for card_set in CardSet.objects.all(): + card_set.delete() + card_sets = {'Pacific': 'Pacific', + 'Fleer': 'Fleer', + 'Bowman': 'Bowman', + 'Leaf': 'Topps', + 'Ultra': 'Fleer', + 'Mystique': 'Fleer', + 'Finest Hour': 'Pacific', + 'SP': 'Upper Deck', + 'SPx': 'Upper Deck', + 'SP Authentic': 'Upper Deck', + 'Black Diamond': 'Upper Deck' + } + for set_name in card_sets: + card_set = CardSet() + card_set.name = set_name + card_set.manufacturer = Manufacturer.objects.get({'name': card_sets.get(set_name)}) + card_set.save() + + +def initialize_parallel_sets(): + """ + Initialize collection ParallelSet with data. + """ + for parallel_set in ParallelSet.objects.all(): + parallel_set.delete() + parallel_sets = {'Mystique Gold': 'Fleer', + 'Pacific Copper': 'Pacific', + 'Pacific Gold': 'Pacific' + } + for key in parallel_sets: + manufacturer = Manufacturer.objects.get({'name': parallel_sets.get(key)}) + parallel_set = ParallelSet() + parallel_set.name = key + parallel_set.manufacturer = manufacturer + parallel_set.save() + + +def initialize_insert_sets(): + """ + Initialize collection InsertSet with data. + """ + for insert_set in InsertSet.objects.all(): + insert_set.delete() + manufacturer = Manufacturer.objects.get({'name': 'Fleer'}) + insert_set = InsertSet() + insert_set.name = 'Mystique Big Buzz' + insert_set.manufacturer = manufacturer + insert_set.save() + + +def initialize_cards(): + """ + Initialize collection Card with data. + """ + for card in Card.objects.all(): + card.delete() + players = ['Pathon, Jerome', 'Bruschi, Tedy', 'Couch, Tim', 'Shea, Aaron', + 'Lewis, Jamal', 'Lewis, Jermaine', 'Banks, Tony', 'Fuamatu-Ma\'Afala, Chris', + 'Bettis, Jerome', 'Stewart, Kordell', 'Moon, Warren', 'Lockett, Kevin', + 'Gannon, Rich', 'Jett, James', 'Strong, Mack', 'Huard, Brock', + 'Watters, Ricky', 'Aikman, Troy', 'LaFleur, David', 'Brazzell, Chris', + 'Dayne, Ron', 'Brown, Na', 'Small, Torrance', 'Lewis, Chad', 'Murrell, Adrian', + 'Smith, Maurice', 'Chandler, Chris', 'Kanell, Danny', 'Williams, Ricky', + 'Garcia, Jeff', 'Streets, Tai', 'Garner, Charlie', 'Rice, Jerry', + 'Owens, Terrell', 'Bruce, Isaac', 'Canidate, Trung'] + cards = [ + [0, 'Indianapolis Colts', 'Pacific', 'Pacific', None, None, False, 2001, 185], + [1, 'Indianapolis Colts', 'Pacific', 'Pacific', None, None, False, 2001, 250], + [2, 'Cleveland Browns', 'Pacific', 'Pacific', None, None, False, 2001, 103], + [3, 'Cleveland Browns', 'Pacific', 'Pacific', None, None, False, 2001, 112], + [4, 'Baltimore Ravens', 'Pacific', 'Pacific', None, None, False, 2001, 37], + [5, 'Baltimore Ravens', 'Pacific', 'Pacific', None, None, False, 2001, 38], + [6, 'Baltimore Ravens', 'Pacific', 'Pacific', None, None, False, 2001, 31], + [7, 'Pittsburgh Steelers', 'Pacific', 'Pacific', None, None, False, 2001, 338], + [8, 'Pittsburgh Steelers', 'Pacific', 'Pacific', None, None, False, 2001, 335], + [9, 'Pittsburgh Steelers', 'Pacific', 'Pacific', None, None, False, 2001, 345], + [10, 'Kansas City Chiefs', 'Pacific', 'Pacific', None, None, False, 2001, 213], + [11, 'Kansas City Chiefs', 'Pacific', 'Pacific', None, None, False, 2001, 212], + [12, 'Oakland Raiders', 'Pacific', 'Pacific', None, None, False, 2001, 311], + [13, 'Oakland Raiders', 'Pacific', 'Pacific', None, None, False, 2001, 312], + [14, 'Seattle Seahawks', 'Pacific', 'Pacific', None, None, False, 2001, 403], + [15, 'Seattle Seahawks', 'Pacific', 'Pacific', None, None, False, 2001, 397], + [16, 'Seattle Seahawks', 'Pacific', 'Pacific', None, None, False, 2001, 404], + [17, 'Dallas Cowboys', 'Pacific', 'Pacific', None, None, False, 2001, 116], + [18, 'Dallas Cowboys', 'Pacific', 'Pacific', None, None, False, 2001, 122], + [19, 'Dallas Cowboys', 'Pacific', 'Pacific', None, None, False, 2001, 117], + [20, 'New York Giants', 'Pacific', 'Pacific', None, None, False, 2001, 281], + [21, 'New York Giants', 'Pacific', 'Pacific', None, None, False, 2001, 321], + [22, 'Philadelphia Eagles', 'Pacific', 'Pacific', None, None, False, 2001, 331], + [23, 'Philadelphia Eagles', 'Pacific', 'Pacific', None, None, False, 2001, 324], + [24, 'Washington Redskins', 'Pacific', 'Pacific', None, None, False, 2001, 445], + [25, 'Atlanta Falcons', 'Pacific', 'Pacific', None, None, False, 2001, 28], + [26, 'Atlanta Falcons', 'Pacific', 'Pacific', None, None, False, 2001, 17], + [27, 'Atlanta Falcons', 'Pacific', 'Pacific', None, None, False, 2001, 23], + [28, 'New Orleans Saints', 'Pacific', 'Pacific', None, None, False, 2001, 273], + [29, 'San Francisco 49ers', 'Pacific', 'Pacific', None, None, False, 2001, 380], + [30, 'San Francisco 49ers', 'Pacific', 'Pacific', None, None, False, 2001, 390], + [31, 'San Francisco 49ers', 'Pacific', 'Pacific', None, None, False, 2001, 381], + [32, 'San Francisco 49ers', 'Pacific', 'Pacific', None, None, False, 2001, 387], + [33, 'San Francisco 49ers', 'Pacific', 'Pacific', None, None, False, 2001, 386], + [34, 'St.Louis Rams', 'Pacific', 'Pacific', None, None, False, 2001, 349], + [35, 'St.Louis Rams', 'Pacific', 'Pacific', None, None, False, 2001, 350], + ] + for card_data in cards: + card = Card() + player_name = players[card_data[0]] + (last_name, first_name) = player_name.split(',') + card.player = Player.objects.get( + {'last_name': last_name.strip(), 'first_name': first_name.strip()} + ) + card.team = Team.objects.get({'name': card_data[1]}) + card.manufacturer = Manufacturer.objects.get({'name': card_data[2]}) + card.card_set = CardSet.objects.get({'name': card_data[3]}) + card.parallel_set = card_data[4] + card.insert_set = card_data[5] + card.rookie = card_data[6] + card.year = card_data[7] + card.number = card_data[8] + card.save() diff --git a/flask/kontor/tysc/forms.py b/flask/kontor/tysc/forms.py new file mode 100644 index 0000000..9cb5c3c --- /dev/null +++ b/flask/kontor/tysc/forms.py @@ -0,0 +1,76 @@ +""" +Define form to edit sport, teams, player, manufacturers and card types +""" +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, SelectField, IntegerField, BooleanField +from wtforms.validators import DataRequired, Optional, NumberRange +from bson import ObjectId + + +class SportForm(FlaskForm): + """ + Form to add and edit a Sport + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class TeamForm(FlaskForm): + """ + Form to add and edit a team + """ + name = StringField('Name', validators=[DataRequired()]) + shortname = StringField('Shortname', validators=[DataRequired()]) + sport = SelectField('Sport', coerce=ObjectId) + submit = SubmitField('Submit') + + +class PlayerForm(FlaskForm): + """ + Form to add and edit a player. + """ + first_name = StringField('First name', validators=[DataRequired()]) + last_name = StringField('Last name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class PositionForm(FlaskForm): + """ + Form to add and edit field positions for sports + """ + name = StringField('Name', validators=[DataRequired()]) + description = StringField('Description', validators=[DataRequired()]) + sport = SelectField('Sport', coerce=ObjectId) + submit = SubmitField('Submit') + + +class ManufacturerForm(FlaskForm): + """ + Form to add and edit a card manufacturer. + """ + name = StringField('Name', validators=[DataRequired()]) + submit = SubmitField('Submit') + + +class CardSetForm(FlaskForm): + """ + Form to add and edit a regular card set + """ + name = StringField('Name', validators=[DataRequired()]) + manufacturer = SelectField('Manufacturer', coerce=ObjectId) + submit = SubmitField('Submit') + + +class CardForm(FlaskForm): + """ + Form to add and edit a trading card + """ + player = SelectField('Player', coerce=ObjectId) + team = SelectField('Team', coerce=ObjectId) + manufacturer = SelectField('Manufacturer', coerce=ObjectId) + card_set = SelectField('Card Set', coerce=ObjectId) + parallel_set = SelectField('Parallel Set', coerce=ObjectId, validators=[Optional()]) + insert_set = SelectField('Inserts', coerce=ObjectId, validators=[Optional()]) + rookie = BooleanField('Rookie', validators=[DataRequired()]) + year = IntegerField('Year', validators=[NumberRange(min=1956, max=2020)]) + number = IntegerField('Number', validators=[DataRequired()]) diff --git a/flask/kontor/tysc/models.py b/flask/kontor/tysc/models.py new file mode 100644 index 0000000..02dc341 --- /dev/null +++ b/flask/kontor/tysc/models.py @@ -0,0 +1,155 @@ +"""This modules declares the model for TradingCards related information.""" +from pymongo.write_concern import WriteConcern +from pymodm import MongoModel, fields + + +class Sport(MongoModel): + """Class Sport represents a sport.""" + name = fields.CharField() + + def __str__(self): + """Returns printable version of Sport object.""" + return self.name + + def __repr__(self): + """Returns printable version of Sport object.""" + return "Sport({})".format(self.name) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Position(MongoModel): + """Class Position represents the position of a player for a sport.""" + name = fields.CharField(max_length=4) + description = fields.CharField(max_length=30) + sport = fields.ReferenceField(Sport) + + def __str__(self): + """Returns printable version of Position object.""" + return "{0}({1})".format(self.name, self.description) + + def __repr__(self): + """Returns printable version of Position object.""" + return "Position({0}, {1})".format(self.name, self.description) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Team(MongoModel): + """Class Team represents a team for a sport.""" + name = fields.CharField(max_length=60) + shortname = fields.CharField(max_length=30) + sport = fields.ReferenceField(Sport, blank=True) + + def __str__(self): + """Returns printable version of Team object.""" + return self.name + + def __repr__(self): + """Returns printable version of Team object.""" + return "Team({0}{1})".format(self.name, self.shortname) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Player(MongoModel): + """ + Class Player represents a player. + """ + first_name = fields.CharField(max_length=60) + last_name = fields.CharField(max_length=60) + + def __str__(self): + """Returns printable version of Team object.""" + return "{0} {1}".format(self.first_name, self.last_name) + + def __repr__(self): + """Returns printable version of Team object.""" + return "Player({0} {1})".format(self.first_name, self.last_name) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Manufacturer(MongoModel): + """ + Class Manufacturer represents a manufacturer of trading cards. + """ + name = fields.CharField() + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class CardSet(MongoModel): + """ + Class CardSet represents the regular card set. + """ + name = fields.CharField() + manufacturer = fields.ReferenceField(Manufacturer) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class ParallelSet(MongoModel): + """ + Class CardSet represents the parallel card set. + """ + name = fields.CharField() + manufacturer = fields.ReferenceField(Manufacturer) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class InsertSet(MongoModel): + """ + Class CardSet represents the inserts card set. + """ + name = fields.CharField() + manufacturer = fields.ReferenceField(Manufacturer) + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) + + +class Card(MongoModel): + """ + Class CardSet represents the regular card set. + """ + # pylint: disable=too-many-instance-attributes + # Nine is reasonable in this case. + player = fields.ReferenceField(Player) + team = fields.ReferenceField(Team) + manufacturer = fields.ReferenceField(Manufacturer) + card_set = fields.ReferenceField(CardSet, blank=True) + parallel_set = fields.ReferenceField(ParallelSet, blank=True) + insert_set = fields.ReferenceField(InsertSet, blank=True) + rookie = fields.BooleanField(default=False) + year = fields.IntegerField(min_value=1956, max_value=2020) + number = fields.IntegerField() + + class Meta: + """Sets the connection and connections details.""" + connection_alias = 'kontor' + write_concern = WriteConcern(j=True) diff --git a/flask/kontor/tysc/views.py b/flask/kontor/tysc/views.py new file mode 100644 index 0000000..836b295 --- /dev/null +++ b/flask/kontor/tysc/views.py @@ -0,0 +1,405 @@ +""" +Define BLueprint and Views for TradeYourSportsCards +""" +from flask import Blueprint, url_for, render_template, redirect, flash +from flask_login import login_required +from bson import ObjectId +from pymongo.errors import PyMongoError +from .forms import SportForm, TeamForm, PlayerForm, PositionForm +from .forms import ManufacturerForm +from .models import Sport, Team, Player, Position +from .models import Manufacturer, CardSet, ParallelSet, InsertSet, Card + + +TYSC = Blueprint('tysc', __name__) + + +@TYSC.route('/sport') +@login_required +def list_sports(): + """ + List sports. + :return: + """ + sports = Sport.objects.all() + return render_template('tysc/sports.html', sports=sports, title="Sports") + + +@TYSC.route('/sport/edit/', methods=['GET', 'POST']) +@login_required +def edit_sport(sport_id): + """ + Edit a sport + """ + sport = Sport.objects.get({'_id': ObjectId(sport_id)}) + form = SportForm(obj=sport) + if form.validate_on_submit(): + sport.name = form.name.data + sport.save() + flash('You have successfully edited the sport.') + return redirect(url_for('tysc.list_sports')) + form.name.data = sport.name + return render_template('simpleform.html', action="Edit", + form=form, sport=sport, title="Edit Sport") + + +@TYSC.route('/sport/add', methods=['GET', 'POST']) +@login_required +def add_sport(): + """ + Add a sport + :return: + """ + form = SportForm() + if form.validate_on_submit(): + sport = Sport() + sport.name = form.name.data + try: + sport.save() + flash('You have successfully added a new sport.') + except PyMongoError: + flash('Error: sport name already exists.') + return redirect(url_for('tysc.list_sports')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Sport") + + +@TYSC.route('/sport/delete/', methods=['GET', 'POST']) +@login_required +def delete_sport(sport_id): + """ + Delete a sport + :param sport_id: + :return: + """ + sport = Sport.objects.raw({'_id': ObjectId(sport_id)}) + if sport: + sport.delete() + flash('You have successfully deleted the sport.') + return redirect(url_for('tysc.list_sports')) + + +@TYSC.route('/team') +@login_required +def list_teams(): + """ + List teams. + :return: + """ + teams = Team.objects.all() + return render_template('tysc/teams.html', teams=teams, title="Teams") + + +@TYSC.route('/team/edit/', methods=['GET', 'POST']) +@login_required +def edit_team(team_id): + """ + Edit a team + """ + team = Team.objects.get({'_id': ObjectId(team_id)}) + form = TeamForm(obj=team) + form.sport.choices = [(s.pk, s.name) for s in Sport.objects.all()] + if team.sport: + form.sport.default = team.sport.pk + form.sport.process_data(team.sport.pk) + if form.validate_on_submit(): + team.name = form.name.data + team.shortname = form.shortname.data + team.sport = form.sport.data + team.save() + flash('You have successfully edited the team.') + return redirect(url_for('tysc.list_teams')) + form.name.data = team.name + return render_template('simpleform.html', action="Edit", + form=form, team=team, title="Edit Team") + + +@TYSC.route('/team/add', methods=['GET', 'POST']) +@login_required +def add_team(): + """ + Add a team + :return: + """ + form = TeamForm() + form.sport.choices = [(s.pk, s.name) for s in Sport.objects.all()] + if form.validate_on_submit(): + team = Team() + team.name = form.name.data + team.shortname = form.shortname.data + team.sport = form.sport.data + try: + team.save() + flash('You have successfully added a new team.') + except PyMongoError: + flash('Error: team name already exists.') + return redirect(url_for('tysc.list_teams')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Team") + + +@TYSC.route('/team/delete/', methods=['GET', 'POST']) +@login_required +def delete_team(team_id): + """ + Delete a team + :param team_id: + :return: + """ + team = Team.objects.raw({'_id': ObjectId(team_id)}) + if team: + team.delete() + return redirect(url_for('tysc.list_teams')) + + +@TYSC.route('/position') +@login_required +def list_positions(): + """ + List positions. + :return: + """ + positions = Position.objects.all() + return render_template('tysc/positions.html', positions=positions, title="Positions") + + +@TYSC.route('/position/edit/', methods=['GET', 'POST']) +@login_required +def edit_position(position_id): + """ + Edit a position + """ + position = Position.objects.get({'_id': ObjectId(position_id)}) + form = PositionForm(obj=position) + form.sport.choices = [(s.pk, s.name) for s in Sport.objects.all()] + if position.sport: + form.sport.default = position.sport.pk + form.sport.process_data(position.sport.pk) + if form.validate_on_submit(): + position.name = form.name.data + position.description = form.description.data + position.sport = form.sport.data + position.save() + flash('You have successfully edited the position.') + return redirect(url_for('tysc.list_positions')) + form.name.data = position.name + form.description.data = position.description + return render_template('simpleform.html', action="Edit", + form=form, position=position, title="Edit position") + + +@TYSC.route('/position/add', methods=['GET', 'POST']) +@login_required +def add_position(): + """ + Add a position + :return: + """ + form = PositionForm() + form.sport.choices = [(s.pk, s.name) for s in Sport.objects.all()] + if form.validate_on_submit(): + position = Position() + position.name = form.name.data + position.description = form.description.data + position.sport = form.sport.data + try: + position.save() + flash('You have successfully added a new position.') + except PyMongoError: + flash('Error: team position already exists.') + return redirect(url_for('tysc.list_positions')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Position") + + +@TYSC.route('/position/delete/', methods=['GET', 'POST']) +@login_required +def delete_position(position_id): + """ + Delete a position + :param position_id: + :return: + """ + position = Position.objects.raw({'_id': ObjectId(position_id)}) + if position: + position.delete() + return redirect(url_for('tysc.list_positions')) + + +@TYSC.route('/player') +@login_required +def list_players(): + """ + List players. + :return: + """ + players = Player.objects.all() + return render_template('tysc/players.html', players=players, title="Players") + + +@TYSC.route('/player/edit/', methods=['GET', 'POST']) +@login_required +def edit_player(player_id): + """ + Edit a player + """ + player = Player.objects.get({'_id': ObjectId(player_id)}) + form = PlayerForm(obj=player) + if form.validate_on_submit(): + player.first_name = form.first_name.data + player.last_name = form.last_name.data + player.save() + flash('You have successfully edited the player.') + return redirect(url_for('tysc.list_players')) + form.first_name.data = player.first_name + form.last_name.data = player.last_name + return render_template('simpleform.html', action="Edit", + form=form, player=player, title="Edit player") + + +@TYSC.route('/player/add', methods=['GET', 'POST']) +@login_required +def add_player(): + """ + Add a player + :return: + """ + form = PlayerForm() + if form.validate_on_submit(): + player = Player() + player.first_name = form.first_name.data + player.last_name = form.last_name.data + try: + player.save() + flash('You have successfully added a new player.') + except PyMongoError: + flash('Error: player name already exists.') + return redirect(url_for('tysc.list_players')) + return render_template('simpleform.html', action="Add", + form=form, title="Add player") + + +@TYSC.route('/player/delete/', methods=['GET', 'POST']) +@login_required +def delete_player(player_id): + """ + Delete a player + :param player_id: + :return: + """ + player = Player.objects.raw({'_id': ObjectId(player_id)}) + if player: + player.delete() + return redirect(url_for('tysc.list_players')) + + +@TYSC.route('/manufacturer') +@login_required +def list_manufacturers(): + """ + List manufacturers. + :return: + """ + manufacturers = Manufacturer.objects.all() + return render_template('tysc/manufacturers.html', + manufacturers=manufacturers, + title="Manufacturers") + + +@TYSC.route('/manufacturer/edit/', methods=['GET', 'POST']) +@login_required +def edit_manufacturer(manufacturer_id): + """ + Edit a manufacturer + """ + manufacturer = Manufacturer.objects.get({'_id': ObjectId(manufacturer_id)}) + form = ManufacturerForm(obj=manufacturer) + if form.validate_on_submit(): + manufacturer.name = form.name.data + manufacturer.save() + flash('You have successfully edited the manufacturer.') + return redirect(url_for('tysc.list_manufacturers')) + form.name.data = manufacturer.name + return render_template('simpleform.html', action="Edit", + form=form, manufacturer=manufacturer, title="Edit Manufacturer") + + +@TYSC.route('/manufacturer/add', methods=['GET', 'POST']) +@login_required +def add_manufacturer(): + """ + Add a manufacturer + :return: + """ + form = ManufacturerForm() + if form.validate_on_submit(): + manufacturer = Manufacturer() + manufacturer.name = form.name.data + try: + manufacturer.save() + flash('You have successfully added a new manufacturer.') + except PyMongoError: + flash('Error: manufacturer name already exists.') + return redirect(url_for('tysc.list_manufacturers')) + return render_template('simpleform.html', action="Add", + form=form, title="Add Manufacturer") + + +@TYSC.route('/manufacturer/delete/', methods=['GET', 'POST']) +@login_required +def delete_manufacturer(manufacturer_id): + """ + Delete a manufacturer + :param manufacturer_id: + :return: + """ + manufacturer = Manufacturer.objects.raw({'_id': ObjectId(manufacturer_id)}) + if manufacturer: + manufacturer.delete() + flash('You have successfully deleted the manufacturer.') + return redirect(url_for('tysc.list_manufacturers')) + + +@TYSC.route('/cardsets') +@login_required +def list_card_sets(): + """ + List card sets. + :return: + """ + card_sets = CardSet.objects.all() + return render_template('tysc/cardsets.html', card_sets=card_sets, title="Card Sets") + + +@TYSC.route('/parallelsets') +@login_required +def list_parallel_sets(): + """ + List card sets. + :return: + """ + parallel_sets = ParallelSet.objects.all() + return render_template('tysc/parallelsets.html', + parallel_sets=parallel_sets, title="Parallel Sets") + + +@TYSC.route('/insertsets') +@login_required +def list_insert_sets(): + """ + List card sets. + :return: + """ + insert_sets = InsertSet.objects.all() + return render_template('tysc/insertsets.html', insert_sets=insert_sets, title="Inserts") + + +@TYSC.route('/cards') +@login_required +def list_cards(): + """ + List card sets. + :return: + """ + cards = Card.objects.all() + return render_template('tysc/cards.html', cards=cards, title="Cards") diff --git a/flask/kontor/version.py b/flask/kontor/version.py new file mode 100644 index 0000000..044c225 --- /dev/null +++ b/flask/kontor/version.py @@ -0,0 +1,2 @@ +"""This module declares the version of the Kontor application.""" +__version__ = '0.0.7' diff --git a/flask/pylint.cfg b/flask/pylint.cfg new file mode 100644 index 0000000..e08fe3a --- /dev/null +++ b/flask/pylint.cfg @@ -0,0 +1,382 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +init-hook='sys.path = list(); sys.path.append("./lib/python3.4/site-packages/")' + +# Profiled execution. +profile=no + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Allow optimization of some AST trees. This will activate a peephole AST +# optimizer, which will apply various small optimizations. For instance, it can +# be used to obtain the result of joining multiple strings with the addition +# operator. Joining a lot of strings can lead to a maximum recursion error in +# Pylint and this flag can prevent that. It has one side effect, the resulting +# AST will be different than the one from reality. +optimize-ast=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=W1601,W1606,W1604,W1630,E1605,E1604,W1636,W1603,W1633,W1610,W1615,W1626,E1606,W0704,I0020,W1622,E1608,W1612,W1638,W1614,W1616,W1605,W1613,W1634,E1603,I0021,W1640,W1621,W1625,E1602,W1639,W1602,W1620,W1617,W1624,W1609,W1618,E1601,E1607,W1637,W1632,W1629,W1635,W1611,W1623,W1608,W1627,W1628,W1607,W1619,E1101,R0201,R0903,R0801,locally-disabled + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=parseable + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells whether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (RP0004). +comment=no + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis +ignored-modules= + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). +ignored-classes=SQLObject + +# When zope mode is activated, add a predefined set of Zope acquired attributes +# to generated-members. +zope=no + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E0201 when accessed. Python regular +# expressions are accepted. +generated-members=REQUEST,acl_users,aq_parent + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_$|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=100 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[BASIC] + +# Required attributes for module, separated by a comma +#required-attributes= + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=yes + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for function names +function-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for attribute names +attr-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for method names +method-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for argument names +argument-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for variable names +variable-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=__.*__ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +#ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=stringprep,optparse + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/flask/requirements.txt b/flask/requirements.txt new file mode 100644 index 0000000..f596076 --- /dev/null +++ b/flask/requirements.txt @@ -0,0 +1,11 @@ +flask +flask-login +flask-bootstrap +Flask-WTF +flask-testing +pymodm +nose +nose-htmloutput +coverage +pylint + diff --git a/flask/tests/__init__.py b/flask/tests/__init__.py new file mode 100644 index 0000000..8506c57 --- /dev/null +++ b/flask/tests/__init__.py @@ -0,0 +1,38 @@ +from flask_testing import TestCase +from kontor import create_app +from kontor.models import User + + +class TestBase(TestCase): + + def create_app(self): + + # pass in test configuration + config_name = 'testing' + app = create_app(config_name) + return app + + def setUp(self): + """ + Will be called before every test + """ + # create test admin user + admin = User() + admin.username="admin" + admin.password="admin2016" + admin.is_admin=True + admin.save() + + # create test non-admin user + employee = User() + employee.username="test_user" + employee.password="test2016" + employee.save() + + def tearDown(self): + """ + Will be called after every test + """ + users = User.objects.all() + for user in users: + user.delete() diff --git a/flask/tests/test_comics_model.py b/flask/tests/test_comics_model.py new file mode 100644 index 0000000..2a5dcc5 --- /dev/null +++ b/flask/tests/test_comics_model.py @@ -0,0 +1,51 @@ +""" +This module cantains tests for the comic data model +""" +import unittest +from . import TestBase +from kontor.comics.models import Artist, Comic, Publisher + + +class TestComicsModel(TestBase): + """This TestCase contains tests for comic data model.""" + + def setUp(self): + publisher = Publisher() + publisher.name = "Publisher1" + publisher.save() + artist = Artist() + artist.name = "Artist1" + artist.save() + comic = Comic() + comic.title = "Title1" + comic.save() + + def tearDown(self): + for comic in Comic.objects.all(): + comic.delete() + for artist in Artist.objects.all(): + artist.delete() + for publisher in Publisher.objects.all(): + publisher.delete() + + def test_comic_model(self): + comic_list = Comic.objects.all() + self.assertEqual(comic_list.count(), 1) + + def test_publisher_model(self): + self.assertEqual(Publisher.objects.all().count(), 1) + + def test_artist_model(self): + self.assertEqual(Artist.objects.all().count(), 1) + + def test_assign_publisher(self): + comic = Comic.objects.first() + publisher = Publisher.objects.first() + comic.publisher = publisher + comic.save() + self.assertEqual(publisher.name, comic.publisher.name) + self.assertEqual(publisher.comics.count(), 1) + + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_comics_view.py b/flask/tests/test_comics_view.py new file mode 100644 index 0000000..e33fd79 --- /dev/null +++ b/flask/tests/test_comics_view.py @@ -0,0 +1,44 @@ +"""This module contains tests for Comic related urls.""" +import unittest +from flask import url_for +from . import TestBase + + +class TestComicsViews(TestBase): + + def test_comics_index(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('comic.list_comics') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_comics_artist(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('comic.list_artists') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_comics_publisher(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('comic.list_publishers') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_config.txt b/flask/tests/test_config.txt new file mode 100644 index 0000000..2b847b0 --- /dev/null +++ b/flask/tests/test_config.txt @@ -0,0 +1,4 @@ +# instance/config.py + +SECRET_KEY = 'p9Bv<3Eid9%$i01' + diff --git a/flask/tests/test_kontor_model.py b/flask/tests/test_kontor_model.py new file mode 100644 index 0000000..0120475 --- /dev/null +++ b/flask/tests/test_kontor_model.py @@ -0,0 +1,14 @@ +""" +This module cantains tests for the general Kontor data model. +""" +from . import TestBase +from kontor.models import User + + +class TestKontorModel(TestBase): + """This TestCase contains tests for users.""" + def test_user_model(self): + """ + Test number of records in User collection + """ + self.assertEqual(User.objects.all().count(), 2) diff --git a/flask/tests/test_kontor_view.py b/flask/tests/test_kontor_view.py new file mode 100644 index 0000000..101ae2f --- /dev/null +++ b/flask/tests/test_kontor_view.py @@ -0,0 +1,58 @@ +"""This module contains tests for Comic related urls.""" +import unittest +from flask import url_for +from . import TestBase + + +class TestKontorViews(TestBase): + + def test_homepage_view(self): + """ + Test that homepage is accessible without login + """ + response = self.client.get(url_for('home.homepage')) + self.assertEqual(response.status_code, 200) + + def test_login_view(self): + """ + Test that login page is accessible without login + """ + response = self.client.get(url_for('auth.login')) + self.assertEqual(response.status_code, 200) + + def test_logout_view(self): + """ + Test that logout link is inaccessible without login + and redirects to login page then to logout + """ + target_url = url_for('auth.logout') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_dashboard_view(self): + """ + Test that dashboard is inaccessible without login + and redirects to login page then to dashboard + """ + target_url = url_for('home.dashboard') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_admin_dashboard_view(self): + """ + Test that dashboard is inaccessible without login + and redirects to login page then to dashboard + """ + target_url = url_for('home.admin_dashboard') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_library_model.py b/flask/tests/test_library_model.py new file mode 100644 index 0000000..635bb58 --- /dev/null +++ b/flask/tests/test_library_model.py @@ -0,0 +1,49 @@ +""" +This module cantains tests for the comic data model +""" +import unittest +from . import TestBase +from kontor.library.models import Author, Publisher, Book + + +class TestComicsModel(TestBase): + """This TestCase contains tests for comic data model.""" + + def setUp(self): + publisher = Publisher() + publisher.name = "Publisher1" + publisher.save() + author = Author() + author.name = "Autor1" + author.save() + book = Book() + book.title = "Title1" + book.save() + + def tearDown(self): + for book in Book.objects.all(): + book.delete() + for author in Author.objects.all(): + author.delete() + for publisher in Publisher.objects.all(): + publisher.delete() + + def test_comic_model(self): + book_list = Book.objects.all() + self.assertEqual(book_list.count(), 1) + + def test_publisher_model(self): + self.assertEqual(Publisher.objects.all().count(), 1) + + def test_artist_model(self): + self.assertEqual(Author.objects.all().count(), 1) + + def test_assign_publisher(self): + book = Book.objects.first() + publisher = Publisher.objects.first() + book.publisher = publisher + book.save() + self.assertEqual(publisher.name, book.publisher.name) + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_library_view.py b/flask/tests/test_library_view.py new file mode 100644 index 0000000..51b2c30 --- /dev/null +++ b/flask/tests/test_library_view.py @@ -0,0 +1,44 @@ +"""This module contains tests for Comic related urls.""" +import unittest +from flask import url_for +from . import TestBase + + +class TestLibraryViews(TestBase): + + def test_library_index(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('library.list_books') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_library_author(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('library.list_authors') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_library_publisher(self): + """ + Test that comics page is inaccessible without login + and redirects to login page then to comics page + """ + target_url = url_for('library.list_publishers') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_tysc_model.py b/flask/tests/test_tysc_model.py new file mode 100644 index 0000000..228cc49 --- /dev/null +++ b/flask/tests/test_tysc_model.py @@ -0,0 +1,53 @@ +""" +This module cantains tests for the TYSC data model +""" +import unittest +from . import TestBase +from kontor.tysc import initialize_model +from kontor.tysc.models import Sport, Position, Team, Player +from kontor.tysc.models import Manufacturer, CardSet, ParallelSet, InsertSet, Card + + +class TestTYSCModel(TestBase): + """ + This TestCase contains tests for TYSC data model. + """ + _initialize_db_ = True + + def setUp(self): + if self._initialize_db_: + initialize_model() + self._initialize_db_ = False + + def tearDown(self): + pass + + def test_sport_model(self): + self.assertEqual(Sport.objects.all().count(), 4) + + def test_position_model(self): + self.assertEqual(Position.objects.all().count(), 25) + + def test_team_model(self): + self.assertEqual(Team.objects.all().count(), 122) + + def test_manufacturer_model(self): + self.assertEqual(Manufacturer.objects.all().count(), 8) + + def test_cardset_model(self): + self.assertEqual(CardSet.objects.all().count(), 11) + + def test_parallelset_model(self): + self.assertEqual(ParallelSet.objects.all().count(), 3) + + def test_insertset_model(self): + self.assertEqual(InsertSet.objects.all().count(), 1) + + def test_playerset_model(self): + self.assertEqual(Player.objects.all().count(), 36) + + def test_card_model(self): + self.assertEqual(Card.objects.all().count(), 36) + +if __name__ == '__main__': + unittest.main() diff --git a/flask/tests/test_tysc_view.py b/flask/tests/test_tysc_view.py new file mode 100644 index 0000000..11fc222 --- /dev/null +++ b/flask/tests/test_tysc_view.py @@ -0,0 +1,110 @@ +"""This module contains tests for TradeYourSportsCards related urls.""" +import unittest +from flask import url_for +from . import TestBase + + +class TestTYSCViews(TestBase): + + def test_tysc_index(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_cards') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_sport(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_sports') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_team(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_teams') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_player(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_players') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_manufacturer(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_manufacturers') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_card_set(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_card_sets') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_parallel_set(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_parallel_sets') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_insert_set(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_insert_sets') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + def test_tysc_card(self): + """ + Test that TYSC page is inaccessible without login + and redirects to login page then to TYSC page + """ + target_url = url_for('tysc.list_cards') + redirect_url = url_for('auth.login', next=target_url) + response = self.client.get(target_url) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, redirect_url) + + +if __name__ == '__main__': + unittest.main() From 28746adfbbcbcfb6896da45877f9a77bf5ed1fd9 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Wed, 8 Jan 2025 22:34:21 +0100 Subject: [PATCH 35/91] import from kontor-flask branch develop/0.1.0 --- flask/.gitignore | 2 + flask/README.md | 2 + flask/app.py | 5 + flask/config.py | 47 -- flask/docs/Makefile | 20 - flask/docs/conf.py | 200 --------- flask/docs/index.rst | 20 - flask/kontor/__init__.py | 115 +++-- flask/kontor/admin/__init__.py | 4 - flask/kontor/admin/forms.py | 17 - flask/kontor/admin/views.py | 63 --- flask/kontor/api/__init__.py | 5 + flask/kontor/api/routes.py | 30 ++ flask/kontor/auth/__init__.py | 26 +- flask/kontor/auth/forms.py | 50 --- flask/kontor/auth/models.py | 45 ++ flask/kontor/auth/views.py | 79 ---- flask/kontor/comics/__init__.py | 12 +- flask/kontor/comics/api.py | 28 ++ flask/kontor/comics/forms.py | 31 -- flask/kontor/comics/models.py | 51 --- flask/kontor/comics/routes.py | 16 + .../kontor/comics/templates/comics/list.html | 28 ++ .../kontor/comics/templates/comics/view.html | 27 ++ flask/kontor/comics/views.py | 222 ---------- flask/kontor/config.py | 10 + flask/kontor/extensions.py | 5 + flask/kontor/home/__init__.py | 3 - flask/kontor/home/views.py | 39 -- flask/kontor/library/__init__.py | 3 - flask/kontor/library/forms.py | 35 -- flask/kontor/library/models.py | 55 --- flask/kontor/library/views.py | 228 ---------- flask/kontor/main/__init__.py | 5 + flask/kontor/main/routes.py | 18 + flask/kontor/media/__init__.py | 9 + flask/kontor/media/api.py | 18 + flask/kontor/media/models.py | 43 ++ flask/kontor/media/routes.py | 16 + .../templates/media/mediafile_detail.html | 29 ++ .../media/templates/media/mediafile_list.html | 30 ++ flask/kontor/models.py | 101 ++--- flask/kontor/office/__init__.py | 3 - flask/kontor/static/css/style.css | 126 ------ flask/kontor/templates/admin/users/user.html | 21 - flask/kontor/templates/admin/users/users.html | 58 --- flask/kontor/templates/auth/login.html | 18 - flask/kontor/templates/auth/register.html | 14 - flask/kontor/templates/base.html | 157 +++---- flask/kontor/templates/comics/artists.html | 58 --- flask/kontor/templates/comics/comics.html | 66 --- flask/kontor/templates/comics/publishers.html | 58 --- .../templates/home/admin_dashboard.html | 31 -- flask/kontor/templates/home/dashboard.html | 20 - flask/kontor/templates/home/index.html | 20 - flask/kontor/templates/index.html | 8 + flask/kontor/templates/library/authors.html | 58 --- flask/kontor/templates/library/books.html | 74 ---- .../kontor/templates/library/publishers.html | 58 --- flask/kontor/templates/simpleform.html | 21 - .../kontor/templates/tysc/manufacturers.html | 58 --- flask/kontor/templates/tysc/players.html | 60 --- flask/kontor/templates/tysc/positions.html | 62 --- flask/kontor/templates/tysc/sports.html | 58 --- flask/kontor/templates/tysc/teams.html | 60 --- flask/kontor/tysc/__init__.py | 377 ---------------- flask/kontor/tysc/forms.py | 76 ---- flask/kontor/tysc/models.py | 155 ------- flask/kontor/tysc/views.py | 405 ------------------ flask/kontor/version.py | 2 - flask/pylint.cfg | 382 ----------------- flask/requirements.txt | 11 - flask/tests/__init__.py | 38 -- flask/tests/test_comics_model.py | 51 --- flask/tests/test_comics_view.py | 44 -- flask/tests/test_config.txt | 4 - flask/tests/test_kontor_model.py | 14 - flask/tests/test_kontor_view.py | 58 --- flask/tests/test_library_model.py | 49 --- flask/tests/test_library_view.py | 44 -- flask/tests/test_tysc_model.py | 53 --- flask/tests/test_tysc_view.py | 110 ----- 82 files changed, 562 insertions(+), 4270 deletions(-) create mode 100644 flask/.gitignore create mode 100644 flask/README.md create mode 100644 flask/app.py delete mode 100644 flask/config.py delete mode 100644 flask/docs/Makefile delete mode 100644 flask/docs/conf.py delete mode 100644 flask/docs/index.rst delete mode 100644 flask/kontor/admin/__init__.py delete mode 100644 flask/kontor/admin/forms.py delete mode 100644 flask/kontor/admin/views.py create mode 100644 flask/kontor/api/__init__.py create mode 100644 flask/kontor/api/routes.py delete mode 100644 flask/kontor/auth/forms.py create mode 100644 flask/kontor/auth/models.py delete mode 100644 flask/kontor/auth/views.py create mode 100644 flask/kontor/comics/api.py delete mode 100644 flask/kontor/comics/forms.py delete mode 100644 flask/kontor/comics/models.py create mode 100644 flask/kontor/comics/routes.py create mode 100644 flask/kontor/comics/templates/comics/list.html create mode 100644 flask/kontor/comics/templates/comics/view.html delete mode 100644 flask/kontor/comics/views.py create mode 100644 flask/kontor/config.py create mode 100644 flask/kontor/extensions.py delete mode 100644 flask/kontor/home/__init__.py delete mode 100644 flask/kontor/home/views.py delete mode 100644 flask/kontor/library/__init__.py delete mode 100644 flask/kontor/library/forms.py delete mode 100644 flask/kontor/library/models.py delete mode 100644 flask/kontor/library/views.py create mode 100644 flask/kontor/main/__init__.py create mode 100644 flask/kontor/main/routes.py create mode 100644 flask/kontor/media/__init__.py create mode 100644 flask/kontor/media/api.py create mode 100644 flask/kontor/media/models.py create mode 100644 flask/kontor/media/routes.py create mode 100644 flask/kontor/media/templates/media/mediafile_detail.html create mode 100644 flask/kontor/media/templates/media/mediafile_list.html delete mode 100644 flask/kontor/office/__init__.py delete mode 100644 flask/kontor/static/css/style.css delete mode 100644 flask/kontor/templates/admin/users/user.html delete mode 100644 flask/kontor/templates/admin/users/users.html delete mode 100644 flask/kontor/templates/auth/login.html delete mode 100644 flask/kontor/templates/auth/register.html delete mode 100644 flask/kontor/templates/comics/artists.html delete mode 100644 flask/kontor/templates/comics/comics.html delete mode 100644 flask/kontor/templates/comics/publishers.html delete mode 100644 flask/kontor/templates/home/admin_dashboard.html delete mode 100644 flask/kontor/templates/home/dashboard.html delete mode 100644 flask/kontor/templates/home/index.html create mode 100644 flask/kontor/templates/index.html delete mode 100644 flask/kontor/templates/library/authors.html delete mode 100644 flask/kontor/templates/library/books.html delete mode 100644 flask/kontor/templates/library/publishers.html delete mode 100644 flask/kontor/templates/simpleform.html delete mode 100644 flask/kontor/templates/tysc/manufacturers.html delete mode 100644 flask/kontor/templates/tysc/players.html delete mode 100644 flask/kontor/templates/tysc/positions.html delete mode 100644 flask/kontor/templates/tysc/sports.html delete mode 100644 flask/kontor/templates/tysc/teams.html delete mode 100644 flask/kontor/tysc/__init__.py delete mode 100644 flask/kontor/tysc/forms.py delete mode 100644 flask/kontor/tysc/models.py delete mode 100644 flask/kontor/tysc/views.py delete mode 100644 flask/kontor/version.py delete mode 100644 flask/pylint.cfg delete mode 100644 flask/requirements.txt delete mode 100644 flask/tests/__init__.py delete mode 100644 flask/tests/test_comics_model.py delete mode 100644 flask/tests/test_comics_view.py delete mode 100644 flask/tests/test_config.txt delete mode 100644 flask/tests/test_kontor_model.py delete mode 100644 flask/tests/test_kontor_view.py delete mode 100644 flask/tests/test_library_model.py delete mode 100644 flask/tests/test_library_view.py delete mode 100644 flask/tests/test_tysc_model.py delete mode 100644 flask/tests/test_tysc_view.py diff --git a/flask/.gitignore b/flask/.gitignore new file mode 100644 index 0000000..259ed2a --- /dev/null +++ b/flask/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +.idea diff --git a/flask/README.md b/flask/README.md new file mode 100644 index 0000000..6aad8a5 --- /dev/null +++ b/flask/README.md @@ -0,0 +1,2 @@ +# Kontor Flask + diff --git a/flask/app.py b/flask/app.py new file mode 100644 index 0000000..dc827b3 --- /dev/null +++ b/flask/app.py @@ -0,0 +1,5 @@ +from kontor import create_app + +if __name__ == '__main__': + app = create_app() + app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/flask/config.py b/flask/config.py deleted file mode 100644 index 4ea5e78..0000000 --- a/flask/config.py +++ /dev/null @@ -1,47 +0,0 @@ -# config.py - - -class Config(object): - """ - Common configurations - """ - - # Put any configurations here that are common across all environments - host = '127.0.0.1' - port = 8500 - database = 'kontor' - - -class DevelopmentConfig(Config): - """ - Development configurations - """ - - DEBUG = True - host = '0.0.0.0' - database = 'kontor_dev' - - -class ProductionConfig(Config): - """ - Production configurations - """ - - DEBUG = False - -class TestingConfig(Config): - """ - Testing configurations - """ - - TESTING = True - DEBUG = True - port = 8600 - database = 'kontor_test' - -app_config = { - 'development': DevelopmentConfig, - 'production': ProductionConfig, - 'testing': TestingConfig -} - diff --git a/flask/docs/Makefile b/flask/docs/Makefile deleted file mode 100644 index d7218b4..0000000 --- a/flask/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXPROJ = KontorFlask -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/flask/docs/conf.py b/flask/docs/conf.py deleted file mode 100644 index 7278f2d..0000000 --- a/flask/docs/conf.py +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Kontor Flask documentation build configuration file, created by -# sphinx-quickstart on Mon Nov 6 08:53:04 2017. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.mathjax', - 'sphinx.ext.ifconfig', - 'sphinx.ext.viewcode'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = 'Kontor Flask' -copyright = '2017, Thomas Peetz' -author = 'Thomas Peetz' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '0.1' -# The full version, including alpha/beta/rc tags. -release = '0.0.7' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = 'de' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'alabaster' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# This is required for the alabaster theme -# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars -html_sidebars = { - '**': [ - 'relations.html', # needs 'show_related': True theme option to display - 'searchbox.html', - ] -} - - -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = 'KontorFlaskdoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'KontorFlask.tex', 'Kontor Flask Documentation', - 'Thomas Peetz', 'manual'), -] - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'kontorflask', 'Kontor Flask Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'KontorFlask', 'Kontor Flask Documentation', - author, 'KontorFlask', 'One line description of project.', - 'Miscellaneous'), -] - - - -# -- Options for Epub output ---------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = project -epub_author = author -epub_publisher = author -epub_copyright = copyright - -# The unique identifier of the text. This can be a ISBN number -# or the project homepage. -# -# epub_identifier = '' - -# A unique identification for the text. -# -# epub_uid = '' - -# A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] - - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} diff --git a/flask/docs/index.rst b/flask/docs/index.rst deleted file mode 100644 index ca4df98..0000000 --- a/flask/docs/index.rst +++ /dev/null @@ -1,20 +0,0 @@ -.. Kontor Flask documentation master file, created by - sphinx-quickstart on Mon Nov 6 08:53:04 2017. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to Kontor Flask's documentation! -======================================== - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/flask/kontor/__init__.py b/flask/kontor/__init__.py index 961e132..f8cb55c 100644 --- a/flask/kontor/__init__.py +++ b/flask/kontor/__init__.py @@ -1,74 +1,61 @@ -""" -Module kontor implements Kontor application. -""" +from flask import Flask, render_template +from flask_jwt_extended import JWTManager -from flask import Flask -from flask_bootstrap import Bootstrap -from flask_login import LoginManager -from pymodm import connect +from kontor import config +from kontor.extensions import db, ma +from logging.config import dictConfig -# local imports -from config import app_config -from .version import __version__ +dictConfig({ + 'version': 1, + 'formatters': {'default': { + 'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', + }}, + 'handlers': {'wsgi': { + 'class': 'logging.StreamHandler', + 'stream': 'ext://flask.logging.wsgi_errors_stream', + 'formatter': 'default' + }}, + 'root': { + 'level': 'INFO', + 'handlers': ['wsgi'] + } +}) + +app = Flask(__name__) -_LOGIN_MANAGER_ = LoginManager() +def create_app(config_class=config.Config): + app.config.from_object(config_class) + db.init_app(app) + ma.init_app(app) + # Initialize Flask extensions here + app.config["JWT_SECRET_KEY"] = "super-secret" # Change this! + jwt = JWTManager(app) -def get_host(config_name): - """ - Returns host address from configuration. - :param config_name: - :return: host address - """ - host = app_config[config_name].host - return host + with app.app_context(): + # db.create_all() + db.reflect() + # Register blueprints here + from kontor.main import bp as main_bp + app.register_blueprint(main_bp) + from kontor.comics import comics_bp + app.register_blueprint(comics_bp, url_prefix='/comics') + from kontor.api import api_bp + app.register_blueprint(api_bp, url_prefix='/api/v1') + from kontor.comics import comics_api + app.register_blueprint(comics_api, url_prefix='/api/v1/comics') -def get_port(config_name): - """ - Returns port number from configuration. - :param config_name: - :return: port number - """ - port = app_config[config_name].port - return port - - -def create_app(config_name): - """ - Returns Flask application. - :param config_name: - :return: Flask application - """ - app = Flask(__name__, instance_relative_config=True) - app.config.from_object(app_config[config_name]) - app.config.from_pyfile('config.py') - - database = "mongodb://localhost:27017/{}".format(app_config[config_name].database) - connect(database, alias="kontor") - - Bootstrap(app) - _LOGIN_MANAGER_.init_app(app) - _LOGIN_MANAGER_.login_message = "You must be logged in to access this page." - _LOGIN_MANAGER_.login_view = "auth.login" - - from .admin.views import ADMIN as ADMIN_BLUEPRINT - app.register_blueprint(ADMIN_BLUEPRINT, url_prefix='/admin') - - from .auth.views import AUTH as AUTH_BLUEPRINT - app.register_blueprint(AUTH_BLUEPRINT) - - from .home.views import HOME as HOME_BLUEPRINT - app.register_blueprint(HOME_BLUEPRINT) - - from .comics.views import COMIC as COMIC_BLUEPRINT - app.register_blueprint(COMIC_BLUEPRINT, url_prefix='/comics') - - from .library.views import LIBRARY as LIBRARY_BLUEPRINT - app.register_blueprint(LIBRARY_BLUEPRINT, url_prefix='/library') - - from .tysc.views import TYSC as TYSC_BLUEPRINT - app.register_blueprint(TYSC_BLUEPRINT, url_prefix='/tysc') + from kontor.media import media_bp + app.register_blueprint(media_bp, url_prefix='/media') + from kontor.media import media_api + app.register_blueprint(media_api, url_prefix='/api/v1/media') + # from kontor.auth.auth import auth_bp + # from kontor.cart.cart import cart_bp + # from kontor.general.general import general_bp + # app.register_blueprint(auth_bp) + # app.register_blueprint(cart_bp, url_prefix='/cart') + # app.register_blueprint(general_bp) return app diff --git a/flask/kontor/admin/__init__.py b/flask/kontor/admin/__init__.py deleted file mode 100644 index ca00a4b..0000000 --- a/flask/kontor/admin/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -""" -Module admin implements administration functions. -""" -from . import views diff --git a/flask/kontor/admin/forms.py b/flask/kontor/admin/forms.py deleted file mode 100644 index d05cfaf..0000000 --- a/flask/kontor/admin/forms.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Define form to edit users -""" -from flask_wtf import FlaskForm -from wtforms import StringField, SubmitField -from wtforms.validators import DataRequired - - -class UserEditForm(FlaskForm): - """ - Form for admin to edit users - """ - email = StringField('Email', validators=[DataRequired()]) - username = StringField('Username', validators=[DataRequired()]) - first_name = StringField('First Name', validators=[DataRequired()]) - last_name = StringField('Last Name', validators=[DataRequired()]) - submit = SubmitField('Submit') diff --git a/flask/kontor/admin/views.py b/flask/kontor/admin/views.py deleted file mode 100644 index e96d8f0..0000000 --- a/flask/kontor/admin/views.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -Define routing rules for managing users -""" -from flask import abort, Blueprint, flash, redirect, render_template, url_for -from flask_login import current_user, login_required - -from .forms import UserEditForm -from ..models import User - -ADMIN = Blueprint('admin', __name__) - - -def check_admin(): - """ - Prevent non-admins from accessing the page - """ - if not current_user.is_admin: - abort(403) - - -# User Views -@ADMIN.route('/users') -@login_required -def list_users(): - """ - List all users - """ - check_admin() - - users = User.objects.all() - return render_template('admin/users/users.html', - users=users, title='Users') - - -@ADMIN.route('/users/edit/', methods=['GET', 'POST']) -@login_required -def edit_user(user_id): - """ - Edit an user. - """ - check_admin() - - user = User.objects.get({'username': user_id}) - - # prevent admin from being assigned a department or role - if user.is_admin: - abort(403) - - form = UserEditForm(obj=user) - if form.validate_on_submit(): - user.email = form.email.data - user.username = form.username.data - user.first_name = form.first_name.data - user.last_name = form.last_name.data - user.save() - flash('You have successfully edited an user.') - - # redirect to the roles page - return redirect(url_for('admin.list_users')) - - return render_template('admin/users/user.html', - user=user, form=form, - title='Edit User') diff --git a/flask/kontor/api/__init__.py b/flask/kontor/api/__init__.py new file mode 100644 index 0000000..b26fcb6 --- /dev/null +++ b/flask/kontor/api/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +api_bp = Blueprint('api_bp', __name__) + +from kontor.api import routes diff --git a/flask/kontor/api/routes.py b/flask/kontor/api/routes.py new file mode 100644 index 0000000..4e9b489 --- /dev/null +++ b/flask/kontor/api/routes.py @@ -0,0 +1,30 @@ +from flask import jsonify, request +from flask_jwt_extended import jwt_required, get_jwt_identity, create_access_token + +from kontor.api import api_bp + + +@api_bp.route('/') +def index(): + modules = ['comics'] + return jsonify(modules) + + +# Create a route to authenticate your users and return JWTs. The +# create_access_token() function is used to actually generate the JWT. +@api_bp.route("/login", methods=["POST"]) +def login(): + username = request.json.get("username", None) + password = request.json.get("password", None) + if username != "test" or password != "test": + return jsonify({"msg": "Bad username or password"}), 401 + + access_token = create_access_token(identity=username) + return jsonify(access_token=access_token) + + +@api_bp.route('/protected', methods=['GET']) +@jwt_required() +def protected(): + current_user = get_jwt_identity() + return {'message': f'Hello, {current_user}!'} diff --git a/flask/kontor/auth/__init__.py b/flask/kontor/auth/__init__.py index 2d9b1ca..9da7988 100644 --- a/flask/kontor/auth/__init__.py +++ b/flask/kontor/auth/__init__.py @@ -1,4 +1,22 @@ -""" -Module auth implements authentication functions. -""" -from . import views +import bcrypt +from flask import session +from flask_httpauth import HTTPBasicAuth + +from kontor import app +from kontor.auth.models import User + +auth = HTTPBasicAuth() + + +@auth.verify_password +def verify_password(username, password): + if username is None: + return False + # Add your authentication logic here + app.logger.info("login user %s", username) + user = User.query.filter_by(user_name=username).first() + app.logger.info("User: %s", user) + app.logger.info("Stored Password: '%s' Hashed Password: '%s'", user.password, bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())) + if 'user_name' in session and session['user_name'] == username: + return True + return False \ No newline at end of file diff --git a/flask/kontor/auth/forms.py b/flask/kontor/auth/forms.py deleted file mode 100644 index 6e4d6dd..0000000 --- a/flask/kontor/auth/forms.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Contains forms for authentication. -""" -from flask_wtf import FlaskForm -from wtforms import PasswordField, StringField, SubmitField, ValidationError -from wtforms.validators import DataRequired, Email, EqualTo - -from ..models import User - - -class RegistrationForm(FlaskForm): - """ - Form for users to create new account - """ - email = StringField('Email', validators=[DataRequired(), Email()]) - username = StringField('Username', validators=[DataRequired()]) - first_name = StringField('First Name', validators=[DataRequired()]) - last_name = StringField('Last Name', validators=[DataRequired()]) - password = PasswordField('Password', validators=[DataRequired(), - EqualTo('confirm_password') - ]) - confirm_password = PasswordField('Confirm Password') - submit = SubmitField('Register') - - def validate_email(self, field): - """ - Check if email is already in use. - :param field: - :return: - """ - if User.objects.raw({'email': field.data}).count() > 0: - raise ValidationError('Email is already in use.') - - def validate_username(self, field): - """ - check if username is already in use. - :param field: - :return: - """ - if User.objects.raw({'username': field.data}).count() > 0: - raise ValidationError('Username is already in use.') - - -class LoginForm(FlaskForm): - """ - Form for users to login - """ - email = StringField('Email', validators=[DataRequired(), Email()]) - password = PasswordField('Password', validators=[DataRequired()]) - submit = SubmitField('Login') diff --git a/flask/kontor/auth/models.py b/flask/kontor/auth/models.py new file mode 100644 index 0000000..fc3b8d4 --- /dev/null +++ b/flask/kontor/auth/models.py @@ -0,0 +1,45 @@ +from kontor.extensions import db, ma +from sqlalchemy.sql import func + + +class User(db.Model): + # __table__ = db.metadata.tables["publisher"] + __tablename__ = "user" + __table_args__ = {'extend_existing': True} + id = db.Column(db.String, primary_key=True) + created_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + last_modified_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + version = db.Column(db.Integer) + enabled = db.Column(db.SmallInteger) + email = db.Column(db.String) + first_name = db.Column(db.String) + last_name = db.Column(db.String) + user_name = db.Column(db.String) + password = db.Column(db.String) + token = db.Column(db.String) + token_expired = db.Column(db.SmallInteger) + + def is_token_valid(self): + return self.review == 'b\x01' + + def is_user_enabled(self): + return self.should_download == 'b\x01' + +class Role(db.Model): + __tablename__ = "role" + __table_args__ = {'extend_existing': True} + id = db.Column(db.String, primary_key=True) + created_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + last_modified_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + version = db.Column(db.Integer) + name = db.Column(db.String) + +class AuthorizationMatrix(db.Model): + __tablename__ = "authorization_matrix" + __table_args__ = {'extend_existing': True} + id = db.Column(db.String, primary_key=True) + created_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + last_modified_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + version = db.Column(db.Integer) + role_id = db.Column(db.String, db.ForeignKey("role.id")) + user_id = db.Column(db.String, db.ForeignKey("user.id")) diff --git a/flask/kontor/auth/views.py b/flask/kontor/auth/views.py deleted file mode 100644 index 217ae18..0000000 --- a/flask/kontor/auth/views.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -Define routing rules for registering users and login and logout. -""" -from flask import Blueprint, flash, redirect, render_template, url_for -from flask_login import login_required, login_user, logout_user -from .forms import LoginForm, RegistrationForm -from ..models import User - -AUTH = Blueprint('auth', __name__) - - -@AUTH.route('/register', methods=['GET', 'POST']) -def register(): - """ - Handle requests to the /register route - Add an employee to the database through the registration form - """ - form = RegistrationForm() - if form.validate_on_submit(): - user = User() - user.email = form.email.data - user.username = form.username.data - user.first_name = form.first_name.data - user.last_name = form.last_name.data - user.password = form.password.data - # add employee to the database - user.save() - flash('You have successfully registered! You may now login.') - - # redirect to the login page - return redirect(url_for('auth.login')) - - # load registration template - return render_template('auth/register.html', form=form, title='Register') - - -@AUTH.route('/login', methods=['GET', 'POST']) -def login(): - """ - Handle requests to the /login route - Validate given email with matching password - :return: - """ - form = LoginForm() - if form.validate_on_submit(): - - # check whether employee exists in the database and whether - # the password entered matches the password in the database - user = User.objects.get({'email': form.email.data}) - if user is not None and user.verify_password( - form.password.data): - # log employee in - login_user(user) - - # redirect to the appropriate dashboard page - if user.is_admin: - return redirect(url_for('home.admin_dashboard')) - else: - return redirect(url_for('home.dashboard')) - - # when login details are incorrect - else: - flash('Invalid email or password.') - - # load login template - return render_template('auth/login.html', form=form, title='Login') - -@AUTH.route('/logout') -@login_required -def logout(): - """ - Handle requests to the /logout route - Log an employee out through the logout link - """ - logout_user() - flash('You have successfully been logged out.') - - # redirect to the login page - return redirect(url_for('auth.login')) diff --git a/flask/kontor/comics/__init__.py b/flask/kontor/comics/__init__.py index e947d82..12bb371 100644 --- a/flask/kontor/comics/__init__.py +++ b/flask/kontor/comics/__init__.py @@ -1,3 +1,9 @@ -""" -Define routing rules for comic related information -""" +from flask import Blueprint + +comics_bp = Blueprint('comics_bp', __name__, + template_folder='templates', + static_folder='static', static_url_path='assets') +comics_api = Blueprint('comics_api', __name__) + +from kontor.comics import routes +from kontor.comics import api diff --git a/flask/kontor/comics/api.py b/flask/kontor/comics/api.py new file mode 100644 index 0000000..22bca28 --- /dev/null +++ b/flask/kontor/comics/api.py @@ -0,0 +1,28 @@ +from flask import Blueprint, render_template, jsonify + +from kontor.comics import comics_api +from kontor.models import Comic, comics_schema, Publisher, comic_schema, publisher_schema, publishers_schema + + +@comics_api.route('/') +def index(): + comics = Comic.query.all() + return comics_schema.dump(comics) + + +@comics_api.route('/comic/') +def view(comic_id): + comic = Comic.query.get(comic_id) + return comic_schema.dump(comic) + + +@comics_api.route('/publisher/') +def publisher_all(): + publishers = Publisher.query.all() + return publishers_schema.dump(publishers) + + +@comics_api.route('/publisher/') +def publisher_detail(publisher_id): + publisher = Publisher.query.get(publisher_id) + return publisher_schema.dump(publisher) \ No newline at end of file diff --git a/flask/kontor/comics/forms.py b/flask/kontor/comics/forms.py deleted file mode 100644 index 0bae45f..0000000 --- a/flask/kontor/comics/forms.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Define form to edit publisher, artists and comics""" -from flask_wtf import FlaskForm -from wtforms import StringField, SubmitField, BooleanField, SelectField -from wtforms.validators import DataRequired -from bson import ObjectId - - -class PublisherForm(FlaskForm): - """ - Form to add and edit a Comic publisher - """ - name = StringField('Name', validators=[DataRequired()]) - submit = SubmitField('Submit') - - -class ArtistForm(FlaskForm): - """ - Form to add and edit a Comic publisher - """ - name = StringField('Name', validators=[DataRequired()]) - submit = SubmitField('Submit') - - -class ComicForm(FlaskForm): - """ - Form to add and edit Comics - """ - title = StringField('Title', validators=[DataRequired()]) - publisher = SelectField('Publisher', coerce=ObjectId) - current_order = BooleanField('Current Order') - submit = SubmitField('Submit') diff --git a/flask/kontor/comics/models.py b/flask/kontor/comics/models.py deleted file mode 100644 index e39e744..0000000 --- a/flask/kontor/comics/models.py +++ /dev/null @@ -1,51 +0,0 @@ -"""This modules declares the model for Comic related information.""" - -from flask import current_app -from pymongo.write_concern import WriteConcern -from pymodm import MongoModel, fields - - -class Publisher(MongoModel): - """Class Publisher represents a publisher of a comic.""" - name = fields.CharField() - - def __str__(self): - return "Publisher({})".format(self.name) - - @property - def comics(self): - """ - Return list of comics which has reference to this publisher - :return: - """ - comics = Comic.objects.raw({'publisher': self.pk}) - current_app.logger.debug(comics) - return comics - - class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) - - -class Artist(MongoModel): - """Class Artist represents a comic artist.""" - name = fields.CharField() - - class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) - - -class Comic(MongoModel): - """Class Comic represents a comic.""" - title = fields.CharField() - publisher = fields.ReferenceField(Publisher) - current_order = fields.BooleanField(default=False) - completed = fields.BooleanField(default=False) - - class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) diff --git a/flask/kontor/comics/routes.py b/flask/kontor/comics/routes.py new file mode 100644 index 0000000..0b0f53e --- /dev/null +++ b/flask/kontor/comics/routes.py @@ -0,0 +1,16 @@ +from flask import Blueprint, render_template + +from kontor.comics import comics_bp +from kontor.models import Comic + + +@comics_bp.route('/') +def index(): + comics = Comic.query.all() + return render_template('comics/list.html', comics=comics) + + +@comics_bp.route('/comic/') +def view(comic_id): + comic = Comic.query.get(comic_id) + return render_template('comics/view.html', comic=comic) diff --git a/flask/kontor/comics/templates/comics/list.html b/flask/kontor/comics/templates/comics/list.html new file mode 100644 index 0000000..c0f4922 --- /dev/null +++ b/flask/kontor/comics/templates/comics/list.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% block content %} + +

{% block title %} Comics {% endblock %}

+
+
+

Comics Blueprint

+ {% for comic in comics %} +
+ +

+ {{ comic.title }} +

+
+

{{ comic.publisher.name }}

+
+

Created

+

{{ comic.created_date }}

+

Modified

+

{{ comic.last_modified_date }}

+
+

Version: {{ comic.version }}

+

Review: {{ comic.review }}

+
+ {% endfor %} +
+{% endblock %} diff --git a/flask/kontor/comics/templates/comics/view.html b/flask/kontor/comics/templates/comics/view.html new file mode 100644 index 0000000..b70da14 --- /dev/null +++ b/flask/kontor/comics/templates/comics/view.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} + +{% block content %} + +

{% block title %} Comics {% endblock %}

+
+
+

Comics Blueprint

+
+ +

+ {{ comic.title }} +

+
+ +

{{ comic.id }}

+
+
+

Created

+

{{ comic.created_date }}

+

Modified

+

{{ comic.last_modified_date }}

+
+

Version: {{ comic.version }}

+
+
+{% endblock %} diff --git a/flask/kontor/comics/views.py b/flask/kontor/comics/views.py deleted file mode 100644 index 090b032..0000000 --- a/flask/kontor/comics/views.py +++ /dev/null @@ -1,222 +0,0 @@ -""" -Define routing rules for comics, publisher and artists -""" -from flask import Blueprint, flash, redirect, render_template, url_for -from flask_login import login_required -from bson import ObjectId -from pymongo.errors import PyMongoError -from .forms import ComicForm, PublisherForm, ArtistForm -from .models import Comic, Publisher, Artist - - -COMIC = Blueprint('comic', __name__) - - -@COMIC.route('/artists') -@login_required -def list_artists(): - """ - List all artists - :return: - """ - artists = Artist.objects.all() - return render_template('comics/artists.html', - artists=artists, title="Artists") - - -@COMIC.route('/artists/edit/', methods=['GET', 'POST']) -@login_required -def edit_artist(artist_id): - """ - Edit a comic artist - """ - artist = Artist.objects.get({'_id': ObjectId(artist_id)}) - form = ArtistForm(obj=artist) - if form.validate_on_submit(): - artist.name = form.name.data - artist.save() - flash('You have successfully edited the artist.') - return redirect(url_for('comic.list_artists')) - form.name.data = artist.name - return render_template('simpleform.html', action="Edit", - form=form, title="Edit Artist") - - -@COMIC.route('/artists/add', methods=['GET', 'POST']) -@login_required -def add_artist(): - """ - Add a artist - :return: - """ - form = ArtistForm() - if form.validate_on_submit(): - artist = Artist() - artist.name = form.name.data - try: - # add publisher to the database - artist.save() - flash('You have successfully added a new artist.') - except PyMongoError: - # in case publisher name already exists - flash('Error: artist name already exists.') - return redirect(url_for('comic.list_artists')) - return render_template('simpleform.html', action="Add", - form=form, title="Add Artist") - - -@COMIC.route('/artists/delete/', methods=['GET', 'POST']) -@login_required -def delete_artist(artist_id): - """ - Delete a comic artist - :param artist_id: - :return: - """ - artist = Artist.objects.raw({'_id': ObjectId(artist_id)}) - if artist: - artist.delete() - flash('You have successfully deleted the comic artist.') - return redirect(url_for('comic.list_artists')) - - -@COMIC.route('/publishers') -@login_required -def list_publishers(): - """ - List all publishers - :return: - """ - publishers = Publisher.objects.all() - return render_template('comics/publishers.html', - publishers=publishers, title="Publishers") - - -@COMIC.route('/publishers/add', methods=['GET', 'POST']) -@login_required -def add_publisher(): - """ - Add a publisher to the database - :return: - """ - form = PublisherForm() - if form.validate_on_submit(): - publisher = Publisher() - publisher.name = form.name.data - try: - # add publisher to the database - publisher.save() - flash('You have successfully added a new publisher.') - except PyMongoError: - # in case publisher name already exists - flash('Error: publisher name already exists.') - return redirect(url_for('comic.list_publishers')) - return render_template('simpleform.html', action="Add", - form=form, title="Add Publisher") - - -@COMIC.route('/publishers/edit/', methods=['GET', 'POST']) -@login_required -def edit_publisher(publisher_id): - """ - Edit a publisher - """ - publisher = Publisher.objects.get({'_id': ObjectId(publisher_id)}) - form = PublisherForm(obj=publisher) - if form.validate_on_submit(): - publisher.name = form.name.data - publisher.save() - flash('You have successfully edited the publisher.') - return redirect(url_for('comic.list_publishers')) - form.name.data = publisher.name - return render_template('simpleform.html', action="Edit", - form=form, title="Edit Publisher") - - -@COMIC.route('/publishers/delete/', methods=['GET', 'POST']) -@login_required -def delete_publisher(publisher_id): - """ - Delete a publisher - :param publisher_id: ObjectId of publisher - :return: - """ - publisher = Publisher.objects.raw({'_id': ObjectId(publisher_id)}) - if publisher: - publisher.delete() - flash('You have successfully deleted the publisher.') - return redirect(url_for('comic.list_publishers')) - - -@COMIC.route('/comics') -@login_required -def list_comics(): - """ - List all comics - :return: - """ - comics = Comic.objects.all() - return render_template('comics/comics.html', - comics=comics, title="Comics") - - -@COMIC.route('/comics/edit/', methods=['GET', 'POST']) -@login_required -def edit_comic(comic_id): - """ - Edit a comic - """ - comic = Comic.objects.get({'_id': ObjectId(comic_id)}) - form = ComicForm(obj=comic) - form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] - form.publisher.default = comic.publisher.pk - form.publisher.process_data(comic.publisher.pk) - if form.validate_on_submit(): - comic.title = form.title.data - comic.current_order = form.current_order.data - comic.publisher = form.publisher.data - comic.save() - flash('You have successfully edited the comic.') - return redirect(url_for('comic.list_comics')) - form.title.data = comic.title - form.current_order.data = comic.current_order - return render_template('simpleform.html', action="Edit", - form=form, title="Edit Comic") - - -@COMIC.route('/comics/add', methods=['GET', 'POST']) -@login_required -def add_comic(): - """ - Add a comic - :return: - """ - form = ComicForm() - form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] - if form.validate_on_submit(): - comic = Comic() - comic.title = form.title.data - comic.publisher = form.publisher.data - try: - comic.save() - flash('You have successfully added a new comic.') - except PyMongoError: - flash('Error: comic title already exists.') - return redirect(url_for('comic.list_comics')) - return render_template('simpleform.html', action="Add", - form=form, title="Add Comic") - - -@COMIC.route('/comics/delete/', methods=['GET', 'POST']) -@login_required -def delete_comic(comic_id): - """ - Delete a comic - :param comic_id: - :return: - """ - comic = Comic.objects.raw({'_id': ObjectId(comic_id)}) - if comic: - comic.delete() - flash('You have successfully deleted the comic.') - return redirect(url_for('comic.list_comics')) diff --git a/flask/kontor/config.py b/flask/kontor/config.py new file mode 100644 index 0000000..8382f66 --- /dev/null +++ b/flask/kontor/config.py @@ -0,0 +1,10 @@ +import os + +basedir = os.path.abspath(os.path.dirname(__file__)) + + +class Config: + SECRET_KEY = os.environ.get('SECRET_KEY') + # SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI') or 'sqlite:///' + os.path.join(basedir, 'kontor.db') + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI') or 'mariadb+mariadbconnector://kontor:kontor@localhost/kontor' + SQLALCHEMY_TRACK_MODIFICATIONS = False diff --git a/flask/kontor/extensions.py b/flask/kontor/extensions.py new file mode 100644 index 0000000..461bd5d --- /dev/null +++ b/flask/kontor/extensions.py @@ -0,0 +1,5 @@ +import flask_sqlalchemy +from flask_marshmallow import Marshmallow + +db = flask_sqlalchemy.SQLAlchemy() +ma = Marshmallow() diff --git a/flask/kontor/home/__init__.py b/flask/kontor/home/__init__.py deleted file mode 100644 index d4468a9..0000000 --- a/flask/kontor/home/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Module for the Kontor homepage. -""" diff --git a/flask/kontor/home/views.py b/flask/kontor/home/views.py deleted file mode 100644 index 4e4d539..0000000 --- a/flask/kontor/home/views.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -Define routing rules for homepage and dashboards -""" - -from flask import Blueprint, render_template, abort -from flask_login import login_required, current_user - -HOME = Blueprint('home', __name__) - - -@HOME.route('/') -def homepage(): - """ - Render the homepage template on the / route - """ - return render_template('home/index.html', title="Welcome") - - -@HOME.route('/dashboard') -@login_required -def dashboard(): - """ - Render the dashboard template on the /dashboard route - """ - return render_template('home/dashboard.html', title="Dashboard") - - -@HOME.route('/admin/dashboard') -@login_required -def admin_dashboard(): - """ - Render the admin_dashboard template on the /admin/dashboard route - :return: - """ - # prevent non-admins from accessing the page - if not current_user.is_admin: - abort(403) - - return render_template('home/admin_dashboard.html', title="Dashboard") diff --git a/flask/kontor/library/__init__.py b/flask/kontor/library/__init__.py deleted file mode 100644 index 31eb0bc..0000000 --- a/flask/kontor/library/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Define routing rules for library related information -""" diff --git a/flask/kontor/library/forms.py b/flask/kontor/library/forms.py deleted file mode 100644 index a080781..0000000 --- a/flask/kontor/library/forms.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Define form to edit publisher, artists and books -""" -from flask_wtf import FlaskForm -from wtforms import StringField, SubmitField, SelectField, IntegerField -from wtforms.validators import DataRequired, Optional, NumberRange -from bson import ObjectId - - -class PublisherForm(FlaskForm): - """ - Form to add and edit a Comic publisher - """ - name = StringField('Name', validators=[DataRequired()]) - submit = SubmitField('Submit') - - -class AuthorForm(FlaskForm): - """ - Form to add and edit a Comic publisher - """ - name = StringField('Name', validators=[DataRequired()]) - submit = SubmitField('Submit') - - -class BookForm(FlaskForm): - """ - Form to add and edit Comics - """ - title = StringField('Title', validators=[DataRequired()]) - author = SelectField('Author', coerce=ObjectId) - publisher = SelectField('Publisher', coerce=ObjectId) - isbn = StringField('ISBN', validators=[Optional()]) - year = IntegerField('Year', validators=[NumberRange(min=1970, max=2050)]) - submit = SubmitField('Submit') diff --git a/flask/kontor/library/models.py b/flask/kontor/library/models.py deleted file mode 100644 index 7ca5c58..0000000 --- a/flask/kontor/library/models.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -This module declares the model for the library related information. -""" -from pymongo.write_concern import WriteConcern -from pymodm import MongoModel, fields - - -class Publisher(MongoModel): - """ - Class Publisher represents a publisher of a book. - """ - name = fields.CharField() - - def __str__(self): - return "Publisher({})".format(self.name) - - class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) - - -class Author(MongoModel): - """ - Class Author represents an author of a book. - """ - name = fields.CharField() - - def __str__(self): - return "Author({})".format(self.name) - - class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) - - -class Book(MongoModel): - """ - Class book represents a book. - """ - title = fields.CharField() - author = fields.ReferenceField(Author) - publisher = fields.ReferenceField(Publisher) - isbn = fields.CharField(blank=True) - year = fields.IntegerField(blank=True) - edition = fields.CharField() - - def __str__(self): - return "Book({})".format(self.title) - - class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) diff --git a/flask/kontor/library/views.py b/flask/kontor/library/views.py deleted file mode 100644 index 1b53eee..0000000 --- a/flask/kontor/library/views.py +++ /dev/null @@ -1,228 +0,0 @@ -"""Define routing rules for book publishers""" -from flask import Blueprint, flash, redirect, render_template, url_for -from flask_login import login_required -from bson import ObjectId -from pymongo.errors import PyMongoError -from .forms import PublisherForm, AuthorForm, BookForm -from .models import Publisher, Author, Book - - -LIBRARY = Blueprint('library', __name__) - - -@LIBRARY.route('/publishers') -@login_required -def list_publishers(): - """ - List all publishers - :return: - """ - publishers = Publisher.objects.all() - return render_template('library/publishers.html', - publishers=publishers, title="Publishers") - - -@LIBRARY.route('/publishers/add', methods=['GET', 'POST']) -@login_required -def add_publisher(): - """ - Add a publisher to the database - :return: - """ - form = PublisherForm() - if form.validate_on_submit(): - publisher = Publisher() - publisher.name = form.name.data - try: - publisher.save() - flash('You have successfully added a new publisher.') - except PyMongoError: - flash('Error: publisher name already exists.') - return redirect(url_for('library.list_publishers')) - return render_template('simpleform.html', action="Add", - form=form, title="Add Publisher") - - -@LIBRARY.route('/publishers/edit/', methods=['GET', 'POST']) -@login_required -def edit_publisher(publisher_id): - """ - Edit a publisher - """ - publisher = Publisher.objects.get({'_id': ObjectId(publisher_id)}) - form = PublisherForm(obj=publisher) - if form.validate_on_submit(): - publisher.name = form.name.data - publisher.save() - flash('You have successfully edited the publisher.') - return redirect(url_for('library.list_publishers')) - form.name.data = publisher.name - return render_template('simpleform.html', action="Edit", - form=form, publisher=publisher, title="Edit Publisher") - - -@LIBRARY.route('/publishers/delete/', methods=['GET', 'POST']) -@login_required -def delete_publisher(publisher_id): - """ - Delete a publisher - :param publisher_id: ObjectId of publisher - :return: - """ - publisher = Publisher.objects.raw({'_id': ObjectId(publisher_id)}) - if publisher: - publisher.delete() - flash('You have successfully deleted the publisher.') - return redirect(url_for('library.list_publishers')) - - -@LIBRARY.route('/authors') -@login_required -def list_authors(): - """ - List all authors - :return: - """ - authors = Author.objects.all() - return render_template('library/authors.html', - authors=authors, title="Authors") - - -@LIBRARY.route('/authors/edit/', methods=['GET', 'POST']) -@login_required -def edit_author(author_id): - """ - Edit a book artist - """ - author = Author.objects.get({'_id': ObjectId(author_id)}) - form = AuthorForm(obj=author) - if form.validate_on_submit(): - author.name = form.name.data - author.save() - flash('You have successfully edited the author.') - return redirect(url_for('library.list_authors')) - form.name.data = author.name - return render_template('simpleform.html', action="Edit", - form=form, author=author, title="Edit Author") - - -@LIBRARY.route('/authors/add', methods=['GET', 'POST']) -@login_required -def add_author(): - """ - Add a author - :return: - """ - form = AuthorForm() - if form.validate_on_submit(): - author = Author() - author.name = form.name.data - try: - author.save() - flash('You have successfully added a new author.') - except PyMongoError: - flash('Error: author name already exists.') - return redirect(url_for('library.list_authors')) - return render_template('simpleform.html', action="Add", - form=form, title="Add Author") - - -@LIBRARY.route('/authors/delete/', methods=['GET', 'POST']) -@login_required -def delete_author(author_id): - """ - Delete a author - :param author_id: - :return: - """ - author = Author.objects.raw({'_id': ObjectId(author_id)}) - if author: - author.delete() - flash('You have successfully deleted the author.') - return redirect(url_for('library.list_authors')) - - -@LIBRARY.route('/books') -@login_required -def list_books(): - """ - List all comics - :return: - """ - books = Book.objects.all() - return render_template('library/books.html', - books=books, title="Books") - - -@LIBRARY.route('/books/edit/', methods=['GET', 'POST']) -@login_required -def edit_book(book_id): - """ - Edit a book - """ - book = Book.objects.get({'_id': ObjectId(book_id)}) - form = BookForm(obj=book) - form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] - if book.publisher: - form.publisher.default = book.publisher.pk - form.publisher.process_data(book.publisher.pk) - form.author.choices = [(a.pk, a.name) for a in Author.objects.all()] - if book.author: - form.author.default = book.author.pk - form.author.process_data(book.author.pk) - if form.validate_on_submit(): - book.title = form.title.data - book.publisher = form.publisher.data - book.author = form.author.data - if form.isbn.data: - book.isbn = form.isbn.data - else: - book.isbn = None - book.year = form.year.data - book.save() - flash('You have successfully edited the book.') - return redirect(url_for('library.list_books')) - form.title.data = book.title - return render_template('simpleform.html', action="Edit", - form=form, book=book, title="Edit Book") - - -@LIBRARY.route('/books/add', methods=['GET', 'POST']) -@login_required -def add_book(): - """ - Add a book - :return: - """ - form = BookForm() - form.publisher.choices = [(p.pk, p.name) for p in Publisher.objects.all()] - form.author.choices = [(a.pk, a.name) for a in Author.objects.all()] - if form.validate_on_submit(): - book = Book() - book.title = form.title.data - book.publisher = form.publisher.data - book.author = form.author.data - book.year = form.year.data - try: - book.save() - flash('You have successfully added a new book.') - except PyMongoError: - flash('Error: book title already exists.') - return redirect(url_for('library.list_books')) - return render_template('simpleform.html', action="Add", - form=form, title="Add Book") - - -@LIBRARY.route('/books/delete/', methods=['GET', 'POST']) -@login_required -def delete_book(book_id): - """ - Delete a book - :param book_id: - :return: - """ - book = Book.objects.raw({'_id': ObjectId(book_id)}) - if book: - book.delete() - flash('You have successfully deleted the book.') - return redirect(url_for('library.list_books')) diff --git a/flask/kontor/main/__init__.py b/flask/kontor/main/__init__.py new file mode 100644 index 0000000..ebdaf6e --- /dev/null +++ b/flask/kontor/main/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +bp = Blueprint('main', __name__) + +from kontor.main import routes diff --git a/flask/kontor/main/routes.py b/flask/kontor/main/routes.py new file mode 100644 index 0000000..0c2e5dd --- /dev/null +++ b/flask/kontor/main/routes.py @@ -0,0 +1,18 @@ +from flask import render_template, session + +from kontor import app +from kontor.main import bp +from kontor.auth import auth + + +@bp.route('/') +@auth.login_required +def index(): + return render_template('index.html') + +@bp.route('/logout') +def logout(): + app.logger.info("logout") + auth.current_user() + session['user_name'] = None + return render_template('index.html') diff --git a/flask/kontor/media/__init__.py b/flask/kontor/media/__init__.py new file mode 100644 index 0000000..581af5f --- /dev/null +++ b/flask/kontor/media/__init__.py @@ -0,0 +1,9 @@ +from flask import Blueprint + +media_bp = Blueprint('media_bp', __name__, + template_folder='templates', + static_folder='static', static_url_path='assets') +media_api = Blueprint('media_api', __name__) + +from kontor.media import routes +from kontor.media import api diff --git a/flask/kontor/media/api.py b/flask/kontor/media/api.py new file mode 100644 index 0000000..be9b875 --- /dev/null +++ b/flask/kontor/media/api.py @@ -0,0 +1,18 @@ +from flask import Blueprint, render_template, jsonify + +from kontor import app +from kontor.media import media_api +from kontor.media.models import MediaFile, mediafile_schema, mediafiles_schema + + +@media_api.route('/') +def mediafile_list(): + app.logger.info("get all media files") + files = MediaFile.query.all() + return mediafiles_schema.dump(files) + + +@media_api.route('/mediafile/') +def mediafile_detail(file_id): + file = MediaFile.query.get(file_id) + return mediafile_schema.dump(file) diff --git a/flask/kontor/media/models.py b/flask/kontor/media/models.py new file mode 100644 index 0000000..4e54978 --- /dev/null +++ b/flask/kontor/media/models.py @@ -0,0 +1,43 @@ +from kontor.extensions import db, ma +from sqlalchemy.sql import func + + +class MediaFile(db.Model): + # __table__ = db.metadata.tables["publisher"] + __tablename__ = "media_file" + __table_args__ = {'extend_existing': True} + id = db.Column(db.String, primary_key=True) + created_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + last_modified_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + version = db.Column(db.Integer) + title = db.Column(db.String) + file_name = db.Column(db.String) + url = db.Column(db.String) + path = db.Column(db.String) + cloud_link = db.Column(db.String) + review = db.Column(db.SmallInteger) + should_download = db.Column(db.Boolean) + + def is_review(self): + return self.review == 'b\x00' + + def is_download(self): + return self.should_download == 'b\x00' + + +class MediaFileSchema(ma.SQLAlchemySchema): + class Meta: + model = MediaFile + fields = ("id", "created_date", "last_modified_date", "version", "title", "file_name", "url", "path", "cloud_link", "review", "should_download", "_links") + + # Smart hyperlinking + _links = ma.Hyperlinks( + { + "self": ma.URLFor("media_api.mediafile_detail", values=dict(mediafile_id="")), + "collection": ma.URLFor("media_api.mediafile_list"), + } + ) + + +mediafile_schema = MediaFileSchema() +mediafiles_schema = MediaFileSchema(many=True) diff --git a/flask/kontor/media/routes.py b/flask/kontor/media/routes.py new file mode 100644 index 0000000..a32f5e3 --- /dev/null +++ b/flask/kontor/media/routes.py @@ -0,0 +1,16 @@ +from flask import Blueprint, render_template + +from kontor.media import media_bp +from kontor.media.models import MediaFile + + +@media_bp.route('/') +def mediafile_list(): + files = MediaFile.query.all() + return render_template('media/mediafile_list.html', mediafiles=files) + + +@media_bp.route('/mediafile/') +def mediafile_detail(mediafile_id): + mediafile = MediaFile.query.get(mediafile_id) + return render_template('media/mediafile_detail.html', mediafile=mediafile) diff --git a/flask/kontor/media/templates/media/mediafile_detail.html b/flask/kontor/media/templates/media/mediafile_detail.html new file mode 100644 index 0000000..e3474ae --- /dev/null +++ b/flask/kontor/media/templates/media/mediafile_detail.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} + +{% block content %} + +

{% block title %} MediaFile {% endblock %}

+
+
+

MediaFile Blueprint

+
+ +

+ {{ mediafile.title }} +

+
+ +

{{ mediafile.id }}

+
+
+

Created

+

{{ mediafile.created_date }}

+

Modified

+

{{ mediafile.last_modified_date }}

+
+

Version: {{ mediafile.version }}

+

Review: {{ mediafile.is_review() }}

+

Should Download: {{mediafile.is_download() }}

+
+
+{% endblock %} diff --git a/flask/kontor/media/templates/media/mediafile_list.html b/flask/kontor/media/templates/media/mediafile_list.html new file mode 100644 index 0000000..2d7d29f --- /dev/null +++ b/flask/kontor/media/templates/media/mediafile_list.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} + +{% block content %} + +

{% block title %} MediaFile {% endblock %}

+
+
+

MediaFile Blueprint

+ {% for mediafile in mediafiles %} +
+ +

+ {{ mediafile.title }} +

+
+

{{ mediafile.id }}

+
+

Created

+

{{ mediafile.created_date }}

+

Modified

+

{{ mediafile.last_modified_date }}

+
+

Version: {{ mediafile.version }}

+

Review: {{ mediafile.is_review() }}

+

Should Download: {{ mediafile.is_download() }}

+

Links: {{ mediafile._links }}

+
+ {% endfor %} +
+{% endblock %} diff --git a/flask/kontor/models.py b/flask/kontor/models.py index 7150364..eba153c 100644 --- a/flask/kontor/models.py +++ b/flask/kontor/models.py @@ -1,66 +1,49 @@ -""" -This modules declares the model for Kontor users. -""" -from pymodm import MongoModel, fields -from pymongo.write_concern import WriteConcern -from flask_login import UserMixin -from werkzeug.security import generate_password_hash, check_password_hash -from kontor import _LOGIN_MANAGER_ +from kontor.extensions import db, ma +from sqlalchemy.sql import func -class User(UserMixin, MongoModel): - """ - This class represents an user for the Kontor application. - """ - email = fields.EmailField() - username = fields.CharField(max_length=60) - first_name = fields.CharField(max_length=60) - last_name = fields.CharField(max_length=60) - password_hash = fields.CharField(max_length=128) - is_admin = fields.BooleanField(default=False) +class Publisher(db.Model): + # __table__ = db.metadata.tables["publisher"] + __tablename__ = "publisher" + __table_args__ = {'extend_existing': True} + id = db.Column(db.String, primary_key=True) + created_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + last_modified_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + version = db.Column(db.Integer) + name = db.Column(db.String) + comics = db.relationship('Comic', back_populates='publisher', lazy='dynamic') - @property - def password(self): - """ - Prevent pasword from being accessed - """ - raise AttributeError('password is not a readable attribute.') - - @password.setter - def password(self, password): - """ - Set password to a hashed password - """ - self.password_hash = generate_password_hash(password) - - def verify_password(self, password): - """ - Check if hashed password matches actual password - """ - return check_password_hash(self.password_hash, password) - - def get_id(self): - """ - Get username as id - :return: - """ - return self.username - - def __repr__(self): - return ''.format(self.username) +class PublisherSchema(ma.SQLAlchemySchema): class Meta: - """Sets the connection and connections details.""" - connection_alias = 'kontor' - write_concern = WriteConcern(j=True) + model = Publisher + + comics = ma.List(ma.HyperlinkRelated("comics_api.index")) -# Set up user_loader -@_LOGIN_MANAGER_.user_loader -def load_user(user_name): - """ - Get list of users from database - :param user_name: - :return: - """ - return User.objects.get({'username': user_name}) +publisher_schema = PublisherSchema() +publishers_schema = PublisherSchema(many=True) + + +class Comic(db.Model): + # __table__ = db.metadata.tables["comic"] + __tablename__ = "comic" + __table_args__ = {'extend_existing': True} + id = db.Column(db.String, primary_key=True) + created_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + last_modified_date = db.Column(db.DateTime(timezone=True), server_default=func.now()) + version = db.Column(db.Integer) + title = db.Column(db.String) + publisher_id = db.Column(db.String, db.ForeignKey("publisher.id")) + publisher = db.relationship("Publisher", back_populates="comics") + + +class ComicSchema(ma.SQLAlchemyAutoSchema): + class Meta: + model = Comic + include_fk = True + publisher = ma.HyperlinkRelated("comics_api.publisher_detail") + + +comic_schema = ComicSchema() +comics_schema = ComicSchema(many=True) diff --git a/flask/kontor/office/__init__.py b/flask/kontor/office/__init__.py deleted file mode 100644 index d830d41..0000000 --- a/flask/kontor/office/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Define routing rules for office related information -""" diff --git a/flask/kontor/static/css/style.css b/flask/kontor/static/css/style.css deleted file mode 100644 index 35fe053..0000000 --- a/flask/kontor/static/css/style.css +++ /dev/null @@ -1,126 +0,0 @@ -/* app/static/css/style.css */ - -body, html { - width: 100%; - height: 100%; -} - -body, h1, h2, h3 { - font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; - font-weight: 700; -} - -a, .navbar-default .navbar-brand, .navbar-default .navbar-nav>li>a { - color: #aec251; -} - -a:hover, .navbar-default .navbar-brand:hover, .navbar-default .navbar-nav>li>a:hover { - color: #687430; -} - -footer { - padding: 50px 0; - background-color: #f8f8f8; -} - -p.copyright { - margin: 15px 0 0; -} - -.alert-info { - width: 50%; - margin: auto; - color: #687430; - background-color: #e6ecca; - border-color: #aec251; -} - -.btn-default { - border-color: #aec251; - color: #aec251; -} - -.btn-default:hover { - background-color: #aec251; -} - -.center { - margin: auto; - width: 70%; - padding: 10px; -} - -.content-section { - padding: 50px 0; - border-top: 1px solid #e7e7e7; -} - -.footer, .push { - clear: both; - height: 4em; -} - -.intro-divider { - width: 400px; - border-top: 1px solid #f8f8f8; - border-bottom: 1px solid rgba(0,0,0,0.2); -} - -.intro-header { - padding-top: 50px; - padding-bottom: 50px; - text-align: center; - color: #f8f8f8; - background: url(../img/intro-bg.jpg) no-repeat center center; - background-size: cover; - height: 100%; -} - -.intro-message { - position: relative; - padding-top: 20%; - padding-bottom: 20%; -} - -.intro-message > h1 { - margin: 0; - text-shadow: 2px 2px 3px rgba(0,0,0,0.6); - font-size: 5em; -} - -.intro-message > h3 { - text-shadow: 2px 2px 3px rgba(0,0,0,0.6); -} - -.lead { - font-size: 18px; - font-weight: 400; -} - -.topnav { - font-size: 14px; -} - -.wrapper { - min-height: 100%; - height: auto !important; - height: 100%; - margin: 0 auto -4em; -} - -.outer { - display: table; - position: absolute; - height: 70%; - width: 100%; -} - -.middle { - display: table-cell; - vertical-align: middle; -} - -.inner { - margin-left: auto; - margin-right: auto; -} diff --git a/flask/kontor/templates/admin/users/user.html b/flask/kontor/templates/admin/users/user.html deleted file mode 100644 index 50df2c4..0000000 --- a/flask/kontor/templates/admin/users/user.html +++ /dev/null @@ -1,21 +0,0 @@ - - -{% import "bootstrap/wtf.html" as wtf %} -{% extends "base.html" %} -{% block title %}Edit User{% endblock %} -{% block body %} -
-
-
-
-
-

Edit User

-
-
- {{ wtf.quick_form(form) }} -
-
-
-
-
-{% endblock %} diff --git a/flask/kontor/templates/admin/users/users.html b/flask/kontor/templates/admin/users/users.html deleted file mode 100644 index b23f67c..0000000 --- a/flask/kontor/templates/admin/users/users.html +++ /dev/null @@ -1,58 +0,0 @@ - - -{% import "bootstrap/utils.html" as utils %} -{% extends "base.html" %} -{% block title %}Users{% endblock %} -{% block body %} -
-
-
-
-
- {{ utils.flashed_messages() }} -
-

Users

- {% if users %} -
-
- - - - - - - - - - - {% for user in users %} - {% if user.is_admin %} - - - - - - - {% else %} - - - - - - - {% endif %} - {% endfor %} - -
Name Email Username Edit
Admin N/A N/A N/A
{{ user.first_name }} {{ user.last_name }} {{ user.email }} {{ user.username }} - - Edit - -
-
- {% endif %} -
-
-
-
-
-{% endblock %} diff --git a/flask/kontor/templates/auth/login.html b/flask/kontor/templates/auth/login.html deleted file mode 100644 index 8b70e01..0000000 --- a/flask/kontor/templates/auth/login.html +++ /dev/null @@ -1,18 +0,0 @@ - - -{% import "bootstrap/utils.html" as utils %} -{% import "bootstrap/wtf.html" as wtf %} -{% extends "base.html" %} -{% block title %}Login{% endblock %} -{% block body %} -
-
- {{ utils.flashed_messages() }} -
-
-

Login to your account

-
- {{ wtf.quick_form(form) }} -
-
-{% endblock %} diff --git a/flask/kontor/templates/auth/register.html b/flask/kontor/templates/auth/register.html deleted file mode 100644 index 55d4918..0000000 --- a/flask/kontor/templates/auth/register.html +++ /dev/null @@ -1,14 +0,0 @@ - - -{% import "bootstrap/wtf.html" as wtf %} -{% extends "base.html" %} -{% block title %}Register{% endblock %} -{% block body %} -
-
-

Register for an account

-
- {{ wtf.quick_form(form) }} -
-
-{% endblock %} diff --git a/flask/kontor/templates/base.html b/flask/kontor/templates/base.html index 80bdc27..0d7af2d 100644 --- a/flask/kontor/templates/base.html +++ b/flask/kontor/templates/base.html @@ -1,107 +1,68 @@ - - {{ title }} | Kontor - - - - - + + {% block title %} {% endblock %} - FlaskApp + - + + + diff --git a/go/templates/kontor/header.html b/go/templates/kontor/header.html new file mode 100644 index 0000000..bcf20aa --- /dev/null +++ b/go/templates/kontor/header.html @@ -0,0 +1,17 @@ + + + + + + + {{ .title }} + + + + + + + + + + diff --git a/go/templates/kontor/index.html b/go/templates/kontor/index.html new file mode 100644 index 0000000..4e23ba7 --- /dev/null +++ b/go/templates/kontor/index.html @@ -0,0 +1,17 @@ + +{{ define "kontor/index.html" }} + +{{ template "header.html" .}} + +{{ template "menu.html" . }} + +
+ {{ if .InfoMessage}} +

{{.InfoMessage}}

+ {{end}} +
+ + +{{ template "footer.html" .}} + +{{ end }} diff --git a/go/templates/kontor/login-successful.html b/go/templates/kontor/login-successful.html new file mode 100644 index 0000000..46e07fa --- /dev/null +++ b/go/templates/kontor/login-successful.html @@ -0,0 +1,12 @@ + + + +{{ template "header.html" .}} +{{ template "menu.html" . }} + +
+ You have successfully logged in. +
+ + +{{ template "footer.html" .}} diff --git a/go/templates/kontor/login.html b/go/templates/kontor/login.html new file mode 100644 index 0000000..83fce69 --- /dev/null +++ b/go/templates/kontor/login.html @@ -0,0 +1,35 @@ + + + +{{ template "header.html" .}} +{{ template "menu.html" . }} + +

Login

+ + +
+
+ + {{ if .ErrorTitle}} +

+ {{.ErrorTitle}}: {{.ErrorMessage}} +

+ {{end}} + +
+
+ + +
+
+ + +
+ +
+
+
+ + + +{{ template "footer.html" .}} diff --git a/go/templates/kontor/menu.html b/go/templates/kontor/menu.html new file mode 100644 index 0000000..b52a358 --- /dev/null +++ b/go/templates/kontor/menu.html @@ -0,0 +1,36 @@ + diff --git a/go/templates/kontor/user-detail.html b/go/templates/kontor/user-detail.html new file mode 100644 index 0000000..983a614 --- /dev/null +++ b/go/templates/kontor/user-detail.html @@ -0,0 +1,59 @@ +{{ define "kontor/user-detail.html" }} +{{ template "header.html" .}} +{{ template "admin-menu.html" .}} + + +
+ + {{ if .ErrorTitle}} +

+ {{.ErrorTitle}}: {{.ErrorMessage}} +

+ {{end}} +
+
+ + {{ if .payload.Username }} + + {{ else }} + + {{ end }} +
+
+
+ + {{ if .payload.Firstname }} + + {{ else }} + + {{ end }} +
+
+ + {{ if .payload.Lastname }} + + {{ else }} + + {{ end }} +
+
+
+ + +
+
+ + +
+ + + {{ if eq .action "Save" }} + + {{ end }} +
+
+{{ template "footer.html" .}} +{{end}} diff --git a/go/templates/kontor/users.html b/go/templates/kontor/users.html new file mode 100644 index 0000000..8aef51c --- /dev/null +++ b/go/templates/kontor/users.html @@ -0,0 +1,38 @@ +{{ define "kontor/users.html" }} +{{ template "header.html" .}} +{{ template "admin-menu.html" .}} + + +
+ + + + + + + + + {{range .payload }} + + + + + {{ if .IsAdmin}} + + {{end}} + {{ if not .IsAdmin }} + + {{end}} + + {{end}} +
Registered users of Kontor
UsernameFirst NameLast NameAdministrator
{{.Username}}{{.Firstname}}{{.Lastname}}
+
+ Add entry +
+
+ +{{ template "footer.html" .}} +{{end}} diff --git a/go/templates/library/authors.html b/go/templates/library/authors.html new file mode 100644 index 0000000..781ec26 --- /dev/null +++ b/go/templates/library/authors.html @@ -0,0 +1,25 @@ + +{{ define "library/authors.html" }} +{{ template "header.html" .}} +{{ template "library/menu.html" .}} + +
+ + + + {{range .payload }} + + {{end}} +
Liste der Autoren
Name
{{.Name}}
+
+ Add entry +
+
+ + +{{ template "footer.html" .}} +{{ end }} diff --git a/go/templates/library/books.html b/go/templates/library/books.html new file mode 100644 index 0000000..456cb82 --- /dev/null +++ b/go/templates/library/books.html @@ -0,0 +1,25 @@ + +{{ define "library/books.html" }} +{{ template "header.html" .}} +{{ template "library/menu.html" . }} + +
+ + + + {{range .payload }} + + {{end}} +
Liste der Bücher
Title
{{.Title}}
+
+ Add entry +
+
+ +{{ template "footer.html" .}} + +{{ end }} diff --git a/go/templates/library/menu.html b/go/templates/library/menu.html new file mode 100644 index 0000000..2e6257f --- /dev/null +++ b/go/templates/library/menu.html @@ -0,0 +1,38 @@ +{{ define "library/menu.html" }} + +{{ end }} diff --git a/go/templates/library/publisher.html b/go/templates/library/publisher.html new file mode 100644 index 0000000..c0dae5e --- /dev/null +++ b/go/templates/library/publisher.html @@ -0,0 +1,32 @@ +{{ define "library/publisher.html" }} +{{ template "header.html" .}} +{{ template "library/menu.html" .}} + +
+ {{ if .ErrorTitle}} +

+ {{.ErrorTitle}}: {{.ErrorMessage}} +

+ {{end}} +
+
+ + {{ if .payload.Name }} + + {{ else }} + + {{ end }} +
+ + + {{ if eq .action "Save" }} + + {{ end }} +
+
+{{ template "footer.html" .}} +{{ end }} diff --git a/go/templates/library/publishers.html b/go/templates/library/publishers.html new file mode 100644 index 0000000..a166167 --- /dev/null +++ b/go/templates/library/publishers.html @@ -0,0 +1,24 @@ + +{{ define "library/publishers.html" }} +{{ template "header.html" .}} +{{ template "library/menu.html" .}} + +
+ + + + {{range .payload }} + + {{end}} +
Liste der Verlage
Name
{{.Name}}
+
+ Add entry +
+
+ +{{ template "footer.html" .}} +{{end}} diff --git a/go/templates/office/index.html b/go/templates/office/index.html new file mode 100644 index 0000000..3932a29 --- /dev/null +++ b/go/templates/office/index.html @@ -0,0 +1,10 @@ + +{{ define "office/index.html" }} +{{ template "header.html" .}} +{{ template "office/menu.html" . }} + +

Home Office

+ +{{ template "footer.html" .}} + +{{ end }} diff --git a/go/templates/office/menu.html b/go/templates/office/menu.html new file mode 100644 index 0000000..ddcd03d --- /dev/null +++ b/go/templates/office/menu.html @@ -0,0 +1,38 @@ +{{ define "office/menu.html" }} + +{{ end }} diff --git a/go/templates/tradingcards/index.html b/go/templates/tradingcards/index.html new file mode 100644 index 0000000..6911e17 --- /dev/null +++ b/go/templates/tradingcards/index.html @@ -0,0 +1,10 @@ + +{{ define "tradingcards/index.html" }} +{{ template "header.html" .}} +{{ template "tradingcards/menu.html" . }} + +

Trading Cards

+ +{{ template "footer.html" .}} + +{{ end }} diff --git a/go/templates/tradingcards/menu.html b/go/templates/tradingcards/menu.html new file mode 100644 index 0000000..524080c --- /dev/null +++ b/go/templates/tradingcards/menu.html @@ -0,0 +1,38 @@ +{{ define "tradingcards/menu.html" }} + +{{ end }} diff --git a/go/templates/tysc/index.html b/go/templates/tysc/index.html new file mode 100644 index 0000000..b1d135d --- /dev/null +++ b/go/templates/tysc/index.html @@ -0,0 +1,16 @@ +{{ define "tysc/index.html" }} +{{ template "header.html" .}} +{{ template "tysc/menu.html" . }} + +{{ template "footer.html" .}} +{{ end }} diff --git a/go/templates/tysc/menu.html b/go/templates/tysc/menu.html new file mode 100644 index 0000000..53a8e9e --- /dev/null +++ b/go/templates/tysc/menu.html @@ -0,0 +1,38 @@ +{{ define "tysc/menu.html" }} + +{{ end }} diff --git a/go/templates/tysc/sports.html b/go/templates/tysc/sports.html new file mode 100644 index 0000000..cf65f25 --- /dev/null +++ b/go/templates/tysc/sports.html @@ -0,0 +1,28 @@ +{{ define "tysc/sports.html" }} +{{ template "header.html" .}} +{{ template "tysc/menu.html" .}} + +
+ + + + {{range .payload }} + + {{end}} +
List of Amertican Sports
Name
{{.Name}}
+
+ Add entry +
+
+{{ template "footer.html" .}} +{{ end }} diff --git a/go/tysc-20041010-1819.sql b/go/tysc-20041010-1819.sql new file mode 100644 index 0000000..167c41d --- /dev/null +++ b/go/tysc-20041010-1819.sql @@ -0,0 +1,168 @@ +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE=NO_AUTO_VALUE_ON_ZERO */; +CREATE DATABASE /*!32312 IF NOT EXISTS*/ `tysc`; +USE `tysc`; + +DROP TABLE IF EXISTS `angebote`; +CREATE TABLE `angebote` ( + `user_id` int(11) NOT NULL default '0', + `karte_id` int(11) NOT NULL default '0' +) TYPE=MyISAM; +INSERT INTO `angebote` (`user_id`,`karte_id`) VALUES (3,28),(3,30); + +DROP TABLE IF EXISTS `benutzer`; +CREATE TABLE `benutzer` ( + `ID` int(11) NOT NULL auto_increment, + `forename` varchar(40) default NULL, + `surname` varchar(40) default NULL, + `strasse` varchar(60) default NULL, + `plz` int(6) default NULL, + `ort` varchar(20) default NULL, + `username` varchar(20) default NULL, + `email` varchar(60) default NULL, + `password` varchar(32) default NULL, + `java` char(1) default NULL, + `language` int(11) NOT NULL default '0', + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `benutzer` (`ID`,`forename`,`surname`,`strasse`,`plz`,`ort`,`username`,`email`,`password`,`java`,`language`) VALUES (1,'Thomas','Peetz','Reichweindamm 24',13627,'Berlin','gophard','thomas.peetz@snafu.de','t.log1n','N',1),(2,'Heiko','John','Johannastr.49',13581,'Berlin','John','healjo@hotmail.com','redskins','N',1),(3,'Thomas','Peetz','Reichweindamm 24',13627,'Berlin','peetz','gophard@snafu.de','peetz','N',1); + +DROP TABLE IF EXISTS `changelog`; +CREATE TABLE `changelog` ( + `datum` date default NULL, + `tablename` varchar(20) default NULL, + `id` int(11) NOT NULL default '0' +) TYPE=MyISAM; +INSERT INTO `changelog` (`datum`,`tablename`,`id`) VALUES ('2002-02-27','benutzer',1),('2002-02-28','benutzer',2),('2002-03-05','benutzer',3),('2002-03-05','angebote',0),('2002-03-05','angebote',0); + +DROP TABLE IF EXISTS `hersteller`; +CREATE TABLE `hersteller` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(30) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `hersteller` (`ID`,`name`) VALUES (1,'Pacific'),(2,'Fleer'),(3,'Bowman'),(6,'Topps'),(7,'Donruss'),(8,'Score'),(9,'Flair'); + +DROP TABLE IF EXISTS `inserts`; +CREATE TABLE `inserts` ( + `ID` int(11) NOT NULL auto_increment, + `hersteller_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`,`hersteller_id`) +) TYPE=MyISAM; +INSERT INTO `inserts` (`ID`,`hersteller_id`,`name`) VALUES (1,2,'Mystique Big Buzz'); + +DROP TABLE IF EXISTS `karte`; +CREATE TABLE `karte` ( + `ID` int(11) NOT NULL auto_increment, + `spieler_id` int(11) NOT NULL default '0', + `team_id` int(11) NOT NULL default '0', + `hersteller_id` int(11) NOT NULL default '0', + `serie_id` int(11) default NULL, + `parallel_id` int(11) default NULL, + `inserts_id` int(11) default NULL, + `rookie` char(1) default NULL, + `jahr` int(4) default NULL, + `nummer` int(11) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `karte` (`ID`,`spieler_id`,`team_id`,`hersteller_id`,`serie_id`,`parallel_id`,`inserts_id`,`rookie`,`jahr`,`nummer`) VALUES (12,12,13,1,1,0,0,'N',2001,212),(1,1,2,1,1,0,0,'N',2001,185),(2,2,2,1,1,0,0,'N',2001,250),(3,3,8,1,1,0,0,'N',2001,103),(4,4,8,1,1,0,0,'N',2001,112),(5,5,6,1,1,0,0,'N',2001,37),(6,6,6,1,1,0,0,'N',2001,38),(7,7,6,1,1,0,0,'N',2001,31),(8,8,10,1,1,0,0,'N',2001,338),(9,9,10,1,1,0,0,'N',2001,335),(10,10,10,1,1,0,0,'N',2001,345),(11,11,13,1,1,0,0,'N',2001,213),(13,13,14,1,1,0,0,'N',2001,311),(14,14,14,1,1,0,0,'N',2001,312),(15,15,16,1,1,0,0,'N',2001,403),(16,16,16,1,1,0,0,'N',2001,397),(17,17,16,1,1,0,0,'N',2001,404),(18,18,18,1,1,0,0,'N',2001,116),(19,19,18,1,1,0,0,'N',2001,122),(20,20,18,1,1,0,0,'N',2001,117),(21,21,19,1,1,0,0,'N',2001,281),(22,22,19,1,1,0,0,'N',2001,321),(23,23,20,1,1,0,0,'N',2001,331),(24,24,20,1,1,0,0,'N',2001,324),(25,25,21,1,1,0,0,'N',2001,445),(26,26,27,1,1,0,0,'N',2001,28),(27,27,27,1,1,0,0,'N',2001,17),(28,28,27,1,1,0,0,'N',2001,23),(29,29,29,1,1,0,0,'N',2001,273); +INSERT INTO `karte` (`ID`,`spieler_id`,`team_id`,`hersteller_id`,`serie_id`,`parallel_id`,`inserts_id`,`rookie`,`jahr`,`nummer`) VALUES (30,30,31,1,1,0,0,'N',2001,380),(31,31,31,1,1,0,0,'N',2001,390),(32,32,31,1,1,0,0,'N',2001,381),(33,33,31,1,1,0,0,'N',2001,387),(34,34,31,1,1,0,0,'N',2001,386),(35,35,30,1,1,0,0,'N',2001,349),(36,36,30,1,1,0,0,'N',2001,350),(37,37,44,5,8,0,0,'N',1994,106); + +DROP TABLE IF EXISTS `language`; +CREATE TABLE `language` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(15) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `mannschaft`; +CREATE TABLE `mannschaft` ( + `ID` int(11) NOT NULL auto_increment, + `team_id` int(11) NOT NULL default '0', + `sportart_id` int(11) NOT NULL default '0', + PRIMARY KEY (`ID`) +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `parallelset`; +CREATE TABLE `parallelset` ( + `ID` int(11) NOT NULL auto_increment, + `hersteller_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`,`hersteller_id`) +) TYPE=MyISAM; +INSERT INTO `parallelset` (`ID`,`hersteller_id`,`name`) VALUES (1,2,'Mystique Gold'),(2,1,'Pacific Copper'),(3,1,'Pacific Gold'); + +DROP TABLE IF EXISTS `position`; +CREATE TABLE `position` ( + `ID` int(11) NOT NULL auto_increment, + `sportart_id` int(11) NOT NULL default '0', + `name` varchar(20) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `position` (`ID`,`sportart_id`,`name`) VALUES (1,1,'QB'),(2,1,'WR'),(3,1,'RB'),(4,1,'LB'),(5,1,'TE'),(6,1,'FB'),(7,1,'SS'),(8,1,'DE'),(9,1,'K'),(10,1,'P'),(11,1,'LG'),(12,1,'RG'),(13,1,'OF'),(14,1,'DB'),(15,1,'CB'),(16,2,'C'),(17,2,'1B'),(18,2,'2B'),(19,2,'3B'),(20,2,'SS'),(21,2,'LF'),(22,2,'CF'),(23,2,'RF'),(24,2,'DH'),(25,2,'P'); + +DROP TABLE IF EXISTS `serie`; +CREATE TABLE `serie` ( + `ID` int(11) NOT NULL auto_increment, + `hersteller_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`,`hersteller_id`) +) TYPE=MyISAM; +INSERT INTO `serie` (`ID`,`hersteller_id`,`name`) VALUES (1,1,'Pacific'),(2,2,'Fleer'),(3,3,'Bowman'),(4,4,'Leaf'),(5,2,'Ultra'),(6,2,'Mystique'),(7,1,'Finest Hour'),(8,5,'SP'),(9,5,'SPX'),(10,5,'SP Authentic'),(11,5,'Black Diamond'); + +DROP TABLE IF EXISTS `spiele`; +CREATE TABLE `spiele` ( + `datum` date default NULL, + `gast` int(11) NOT NULL default '0', + `gast_pkt` int(11) default NULL, + `heim` int(11) NOT NULL default '0', + `heim_pkt` int(11) default NULL +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `spieler`; +CREATE TABLE `spieler` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `spieler` (`ID`,`name`) VALUES (1,'Pathon, Jerome'),(2,'Bruschi, Tedy'),(3,'Couch, Tim'),(4,'Shea, Aaron'),(5,'Lewis, Jamal'),(6,'Lewis, Jermaine'),(7,'Banks, Tony'),(8,'Fuamatu-Ma\'Afala, Chris'),(9,'Bettis, Jerome'),(10,'Stewart, Kordell'),(11,'Moon, Warren'),(12,'Lockett, Kevin'),(13,'Gannon, Rich'),(14,'Jett, James'),(15,'Strong, Mack'),(16,'Huard, Brock'),(17,'Watters, Ricky'),(18,'Aikman, Troy'),(19,'LaFleur, David'),(20,'Brazzell, Chris'),(21,'Dayne, Ron'),(22,'Brown, Na'),(23,'Small, Torrance'),(24,'Lewis, Chad'),(25,'Murrell, Adrian'),(26,'Smith, Maurice'),(27,'Chandler, Chris'),(28,'Kanell, Danny'),(29,'Williams, Ricky'),(30,'Garcia, Jeff'),(31,'Streets, Tai'),(32,'Garner, Charlie'),(33,'Rice, Jerry'),(34,'Owens, Terrell'),(35,'Bruce, Isaac'),(36,'Canidate, Trung'); + +DROP TABLE IF EXISTS `spielerposition`; +CREATE TABLE `spielerposition` ( + `spieler_id` int(11) NOT NULL default '0', + `sportart_id` int(11) NOT NULL default '0', + `position_id` int(11) NOT NULL default '0' +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `sportart`; +CREATE TABLE `sportart` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(30) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `sportart` (`ID`,`name`) VALUES (1,'Football'),(2,'Baseball'),(3,'Basketball'),(4,'Hockey'); + +DROP TABLE IF EXISTS `suche`; +CREATE TABLE `suche` ( + `user_id` int(11) NOT NULL default '0', + `karte_id` int(11) NOT NULL default '0' +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `team`; +CREATE TABLE `team` ( + `ID` int(11) NOT NULL auto_increment, + `sportart_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + `short` varchar(15) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (1,1,'Buffalo Bills','Bills'),(2,1,'Indianapolis Colts','Colts'),(3,1,'Miami Dolphins','Dolphins'),(4,1,'New England Patriots','Patriots'),(5,1,'New York Jets','Jets'),(6,1,'Baltimore Ravens','Ravens'),(7,1,'Cincinnati Bengals','Bengals'),(8,1,'Cleveland Browns','Browns'),(9,1,'Jacksonville Jaguars','Jaguars'),(10,1,'Pittsburgh Steelers','Steelers'),(11,1,'Tennessee Titans','Titans'),(12,1,'Denver Broncos','Broncos'),(13,1,'Kansas City Chiefs','Chiefs'),(14,1,'Oakland Raiders','Raiders'),(15,1,'San Diego Chargers','Chargers'),(16,1,'Seattle Seahawks','Seahawks'),(17,1,'Arizona Cardinals','Cardinals'),(18,1,'Dallas Cowboys','Cowboys'),(19,1,'New York Giants','Giants'),(20,1,'Philadelphia Eagles','Eagles'),(21,1,'Washington Redskins','Redskins'),(22,1,'Chicago Bears','Bears'),(23,1,'Detroit Lions','Lions'),(24,1,'Green Bay Packers','Packers'),(25,1,'Minnesota Vikings','Vikings'),(26,1,'Tampa Bay Buccaneers','Buccaneers'),(27,1,'Atlanta Falcons','Falcons'),(28,1,'Carolina Panthers','Panthers'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (29,1,'New Orleans Saints','Saints'),(30,1,'St.Louis Rams','Rams'),(31,1,'San Francisco 49ers','49ers'),(32,2,'Baltimore Orioles','Orioles'),(33,2,'Boston Red Sox','Red Sox'),(34,2,'New York Yankees','Yankees'),(35,2,'Tampa Bay Devil Rays','Devil Rays'),(36,2,'Toronto Blue Jays','Blue Jays'),(37,2,'Chicago White Sox','White Sox'),(38,2,'Cleveland Indians','Indians'),(39,2,'Detroit Tigers','Tigers'),(40,2,'Kansas City Royals','Royals'),(41,2,'Minnesota Twins','Twins'),(42,2,'Anaheim Angels','Angels'),(43,2,'Oakland Athletics','Athletics'),(44,2,'Seattle Mariners','Mariners'),(45,2,'Texas Rangers','Rangers'),(46,2,'Atlanta Braves','Braves'),(47,2,'Florida Marlins','Marlins'),(48,2,'Montreal Expos','Expos'),(49,2,'New York Mets','Mets'),(50,2,'Philadelphia Phillies','Phillies'),(51,2,'Chicago Cubs','Cubs'),(52,2,'Cincinnati Reds','Reds'),(53,2,'Houston Astros','Astros'),(54,2,'Milwaukee Brewers','Brewers'),(55,2,'Pittsburgh Pirates','Pirates'),(56,2,'St.Louis Cardinals','Cardinals'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (57,2,'Arizona Diamondbacks','Diamondbacks'),(58,2,'Colorado Rockies','Rockies'),(59,2,'Los Angeles Dodgers','Dodgers'),(60,2,'San Diego Padres','Padres'),(61,2,'San Francisco Giants','Giants'),(62,3,'Boston Celtics','Celtics'),(63,3,'Miami Heat','Heat'),(64,3,'New Jersey Nets','Mets'),(65,3,'New York Knicks','Knicks'),(66,3,'Orlando Magic','Magic'),(67,3,'Philadelphia 76ers','76ers'),(68,3,'Washington Wizards','Wizards'),(69,3,'Atlanta Hawks','Hawks'),(70,3,'Charlotte Hornets','Hornets'),(71,3,'Chicago Bulls','Bulls'),(72,3,'Cleveland Cavaliers','Cavaliers'),(73,3,'Detroit Pistons','Pistons'),(74,3,'Indiana Pacers','Pacers'),(75,3,'Milwaukee Bucks','Bucks'),(76,3,'Toronto Raptors','Raptors'),(77,3,'Dallas Mavericks','Mavericks'),(78,3,'Denver Nuggets','Nuggets'),(79,3,'Houston Rockets','Rockets'),(80,3,'Minnesota Timberwolves','Timberwolves'),(81,3,'San Antonio Spurs','Spurs'),(82,3,'Utah Jazz','Jazz'),(83,3,'Vancouver Grizzlies','Grizzlies'),(84,3,'Golden State Warriors','Warriors'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (85,3,'Los Angeles Clippers','Clippers'),(86,3,'Los Angeles Lakers','Lakers'),(87,3,'Phoenix Suns','Suns'),(88,3,'Portland Trail Blazers','Blazers'),(89,3,'Sacramento Kings','Kings'),(90,3,'Seattle SuperSonics','SuperSonics'),(91,4,'Boston Bruins','Bruins'),(92,4,'Buffalo Sabres','Sabres'),(93,4,'Montreal Canadiens','Canadiens'),(94,4,'Ottawa Senators','Senators'),(95,4,'Toronto Maple Leafs','Maple Leafs'),(96,4,'New Jersey Devils','Devils'),(97,4,'New York Islander','Islander'),(98,4,'New York Rangers','Rangers'),(99,4,'Philadelphia Flyers','Flyers'),(100,4,'Pittsburgh Penguins','Penguins'),(101,4,'Atlanta Trashers','Trashers'),(102,4,'Carolina Hurricanes','Hurricanes'),(103,4,'Florida Panthers','Panthers'),(104,4,'Tampa Bay Lightnings','Lightnings'),(105,4,'Washington Capitals','Capitals'),(106,4,'Chicago Blackhawks','Blackhawks'),(107,4,'Columbo Blue Jackets','Blue Jackets'),(108,4,'Detroit Red Wings','Red Wings'),(109,4,'Nashville Predators','Predators'),(110,4,'St.Louis Blues','Blues'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (111,4,'Calgary Flames','Flames'),(112,4,'Colorado Avalanche','Avalanche'),(113,4,'Edmonton Oilers','Oilers'),(114,4,'Minnesota Wild','Wild'),(115,4,'Vancouver Canucks','Canucks'),(116,4,'Anaheim Mighty Ducks','Mighty Ducks'),(117,4,'Dallas Stars','Stars'),(118,4,'Los Angeles Kings','Kings'),(119,4,'Phoenix Coyotes','Coyotes'),(120,4,'San Jose Sharks','Sharks'),(121,1,'Houston Texans','Texans'),(122,1,'Houston Oilers','Oilers'); +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; diff --git a/kotlin-quarkus/.dockerignore b/kotlin-quarkus/.dockerignore new file mode 100644 index 0000000..4361d2f --- /dev/null +++ b/kotlin-quarkus/.dockerignore @@ -0,0 +1,5 @@ +* +!build/*-runner +!build/*-runner.jar +!build/lib/* +!build/quarkus-app/* \ No newline at end of file diff --git a/kotlin-quarkus/.gitignore b/kotlin-quarkus/.gitignore new file mode 100644 index 0000000..285b6ba --- /dev/null +++ b/kotlin-quarkus/.gitignore @@ -0,0 +1,36 @@ +# Gradle +.gradle/ +build/ + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env diff --git a/kotlin-quarkus/README.md b/kotlin-quarkus/README.md new file mode 100644 index 0000000..08f2a76 --- /dev/null +++ b/kotlin-quarkus/README.md @@ -0,0 +1,78 @@ +# kontor Project + +This project uses Quarkus, the Supersonic Subatomic Java Framework. + +If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ . + +## Running the application in dev mode + +You can run your application in dev mode that enables live coding using: +```shell script +./gradlew quarkusDev +``` + +> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. + +## Packaging and running the application + +The application can be packaged using: +```shell script +./gradlew build +``` +It produces the `quarkus-run.jar` file in the `build/quarkus-app/` directory. +Be aware that it’s not an _über-jar_ as the dependencies are copied into the `build/quarkus-app/lib/` directory. + +The application is now runnable using `java -jar build/quarkus-app/quarkus-run.jar`. + +If you want to build an _über-jar_, execute the following command: +```shell script +./gradlew build -Dquarkus.package.type=uber-jar +``` + +The application, packaged as an _über-jar_, is now runnable using `java -jar build/*-runner.jar`. + +## Creating a native executable + +You can create a native executable using: +```shell script +./gradlew build -Dquarkus.package.type=native +``` + +Or, if you don't have GraalVM installed, you can run the native executable build in a container using: +```shell script +./gradlew build -Dquarkus.package.type=native -Dquarkus.native.container-build=true +``` + +You can then execute your native executable with: `./build/kontor-1.0.0-SNAPSHOT-runner` + +If you want to learn more about building native executables, please consult https://quarkus.io/guides/gradle-tooling. + +## Related Guides + +- JDBC Driver - H2 ([guide](https://quarkus.io/guides/datasource)): Connect to the H2 database via JDBC +- Hibernate ORM with Panache and Kotlin ([guide](https://quarkus.io/guides/hibernate-orm-panache-kotlin)): Define your persistent model in Hibernate ORM with Panache +- SmallRye OpenAPI ([guide](https://quarkus.io/guides/openapi-swaggerui)): Document your REST APIs with OpenAPI - comes with Swagger UI +- YAML Configuration ([guide](https://quarkus.io/guides/config#yaml)): Use YAML to configure your Quarkus application +- SmallRye Health ([guide](https://quarkus.io/guides/microprofile-health)): Monitor service health + +## Provided Code + +### YAML Config + +Configure your application with YAML + +[Related guide section...](https://quarkus.io/guides/config-reference#configuration-examples) + +The Quarkus application configuration is located in `src/main/resources/application.yml`. + +### RESTEasy Reactive + +Easily start your Reactive RESTful Web Services + +[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources) + +### SmallRye Health + +Monitor your application's health using SmallRye Health + +[Related guide section...](https://quarkus.io/guides/smallrye-health) diff --git a/kotlin-quarkus/build.gradle.kts b/kotlin-quarkus/build.gradle.kts new file mode 100644 index 0000000..0a6ac3a --- /dev/null +++ b/kotlin-quarkus/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + java + id("io.quarkus") +} + +repositories { + mavenCentral() + mavenLocal() +} + +val quarkusPlatformGroupId: String by project +val quarkusPlatformArtifactId: String by project +val quarkusPlatformVersion: String by project + +dependencies { + implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")) + implementation("io.quarkus:quarkus-resteasy-reactive-jackson") + implementation("io.quarkus:quarkus-jdbc-h2") + implementation("io.quarkus:quarkus-hibernate-orm-panache-kotlin") + implementation("io.quarkus:quarkus-smallrye-openapi") + implementation("io.quarkus:quarkus-config-yaml") + implementation("io.quarkus:quarkus-hibernate-reactive-panache-kotlin") + implementation("io.quarkus:quarkus-smallrye-health") + implementation("io.quarkus:quarkus-arc") + implementation("io.quarkus:quarkus-resteasy-reactive") + testImplementation("io.quarkus:quarkus-junit5") + testImplementation("io.rest-assured:rest-assured") +} + +group = "de.thpeetz.kontor" +version = "1.0.0-SNAPSHOT" + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +tasks.withType { + systemProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager") +} + +tasks.withType { + options.encoding = "UTF-8" + options.compilerArgs.add("-parameters") +} diff --git a/kotlin-quarkus/gradle.properties b/kotlin-quarkus/gradle.properties new file mode 100644 index 0000000..1057e5c --- /dev/null +++ b/kotlin-quarkus/gradle.properties @@ -0,0 +1,6 @@ +#Gradle properties +quarkusPluginId=io.quarkus +quarkusPluginVersion=2.15.2.Final +quarkusPlatformGroupId=io.quarkus.platform +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformVersion=2.15.2.Final \ No newline at end of file diff --git a/kotlin-quarkus/gradle/wrapper/gradle-wrapper.jar b/kotlin-quarkus/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..62d4c053550b91381bbd28b1afc82d634bf73a8a GIT binary patch literal 58910 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 literal 0 HcmV?d00001 diff --git a/kotlin-quarkus/gradle/wrapper/gradle-wrapper.properties b/kotlin-quarkus/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ae04661 --- /dev/null +++ b/kotlin-quarkus/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/kotlin-quarkus/gradlew b/kotlin-quarkus/gradlew new file mode 100755 index 0000000..fbd7c51 --- /dev/null +++ b/kotlin-quarkus/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/kotlin-quarkus/gradlew.bat b/kotlin-quarkus/gradlew.bat new file mode 100644 index 0000000..a9f778a --- /dev/null +++ b/kotlin-quarkus/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kotlin-quarkus/settings.gradle.kts b/kotlin-quarkus/settings.gradle.kts new file mode 100644 index 0000000..043f9fc --- /dev/null +++ b/kotlin-quarkus/settings.gradle.kts @@ -0,0 +1,13 @@ +pluginManagement { + val quarkusPluginVersion: String by settings + val quarkusPluginId: String by settings + repositories { + mavenCentral() + gradlePluginPortal() + mavenLocal() + } + plugins { + id(quarkusPluginId) version quarkusPluginVersion + } +} +rootProject.name="kontor" diff --git a/kotlin-quarkus/src/main/docker/Dockerfile.jvm b/kotlin-quarkus/src/main/docker/Dockerfile.jvm new file mode 100644 index 0000000..669d901 --- /dev/null +++ b/kotlin-quarkus/src/main/docker/Dockerfile.jvm @@ -0,0 +1,94 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./gradlew build +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/kontor-jvm . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-jvm +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-jvm +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 + +ENV LANGUAGE='en_US:en' + + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=185 build/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 build/quarkus-app/*.jar /deployments/ +COPY --chown=185 build/quarkus-app/app/ /deployments/app/ +COPY --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 +ENV AB_JOLOKIA_OFF="" +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + diff --git a/kotlin-quarkus/src/main/docker/Dockerfile.legacy-jar b/kotlin-quarkus/src/main/docker/Dockerfile.legacy-jar new file mode 100644 index 0000000..ab8ff1f --- /dev/null +++ b/kotlin-quarkus/src/main/docker/Dockerfile.legacy-jar @@ -0,0 +1,90 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=legacy-jar +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/kontor-legacy-jar . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-legacy-jar +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-legacy-jar +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 + +ENV LANGUAGE='en_US:en' + + +COPY build/lib/* /deployments/lib/ +COPY build/*-runner.jar /deployments/quarkus-run.jar + +EXPOSE 8080 +USER 185 +ENV AB_JOLOKIA_OFF="" +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" diff --git a/kotlin-quarkus/src/main/docker/Dockerfile.native b/kotlin-quarkus/src/main/docker/Dockerfile.native new file mode 100644 index 0000000..38e4141 --- /dev/null +++ b/kotlin-quarkus/src/main/docker/Dockerfile.native @@ -0,0 +1,27 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=native +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t quarkus/kontor . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor +# +### +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root build/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/kotlin-quarkus/src/main/docker/Dockerfile.native-micro b/kotlin-quarkus/src/main/docker/Dockerfile.native-micro new file mode 100644 index 0000000..cce62e8 --- /dev/null +++ b/kotlin-quarkus/src/main/docker/Dockerfile.native-micro @@ -0,0 +1,30 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# It uses a micro base image, tuned for Quarkus native executables. +# It reduces the size of the resulting container image. +# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=native +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/kontor . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor +# +### +FROM quay.io/quarkus/quarkus-micro-image:1.0 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root build/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/kotlin-quarkus/src/main/resources/META-INF/resources/index.html b/kotlin-quarkus/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000..f64aa10 --- /dev/null +++ b/kotlin-quarkus/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,289 @@ + + + + + kontor - 1.0.0-SNAPSHOT + + + +
+
+
+ + + + + quarkus_logo_horizontal_rgb_1280px_reverse + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+

You just made a Quarkus application.

+

This page is served by Quarkus.

+ Visit the Dev UI +

This page: src/main/resources/META-INF/resources/index.html

+

App configuration: src/main/resources/application.yml

+

Static assets: src/main/resources/META-INF/resources/

+

Code: src/main/java

+

Generated starter code:

+
    +
  • + RESTEasy Reactive Easily start your Reactive RESTful Web Services +
    @Path: /hello +
    Related guide +
  • + +
+
+
+

Selected extensions

+
    +
  • RESTEasy Reactive Jackson
  • +
  • JDBC Driver - H2 (guide)
  • +
  • Hibernate ORM with Panache and Kotlin (guide)
  • +
  • SmallRye OpenAPI (guide)
  • +
  • YAML Configuration (guide)
  • +
  • Hibernate Reactive Panache Kotlin
  • +
  • SmallRye Health (guide)
  • +
+
Documentation
+

Practical step-by-step guides to help you achieve a specific goal. Use them to help get your work + done.

+
Set up your IDE
+

Everyone has a favorite IDE they like to use to code. Learn how to configure yours to maximize your + Quarkus productivity.

+
+
+
+ + diff --git a/kotlin-quarkus/src/main/resources/application.yml b/kotlin-quarkus/src/main/resources/application.yml new file mode 100644 index 0000000..527a35f --- /dev/null +++ b/kotlin-quarkus/src/main/resources/application.yml @@ -0,0 +1,2 @@ +greeting: + message: "hello" diff --git a/kotlin-quarkus/src/native-test/java/de/thpeetz/kontor/GreetingResourceIT.java b/kotlin-quarkus/src/native-test/java/de/thpeetz/kontor/GreetingResourceIT.java new file mode 100644 index 0000000..ef71448 --- /dev/null +++ b/kotlin-quarkus/src/native-test/java/de/thpeetz/kontor/GreetingResourceIT.java @@ -0,0 +1,8 @@ +package de.thpeetz.kontor; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class GreetingResourceIT extends GreetingResourceTest { + // Execute the same tests but in packaged mode. +} diff --git a/kotlin-spring/.gitignore b/kotlin-spring/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/kotlin-spring/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/kotlin-spring/README.md b/kotlin-spring/README.md new file mode 100644 index 0000000..0473319 --- /dev/null +++ b/kotlin-spring/README.md @@ -0,0 +1,92 @@ +# Kontor Kotlin + + + +## Getting started + +To make it easy for you to get started with GitLab, here's a list of recommended next steps. + +Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! + +## Add your files + +- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files +- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: + +``` +cd existing_repo +git remote add origin https://gitlab.thpeetz.de/kontor/kontor-kotlin.git +git branch -M main +git push -uf origin main +``` + +## Integrate with your tools + +- [ ] [Set up project integrations](https://gitlab.thpeetz.de/kontor/kontor-kotlin/-/settings/integrations) + +## Collaborate with your team + +- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) +- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) +- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) +- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) +- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) + +## Test and Deploy + +Use the built-in continuous integration in GitLab. + +- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) +- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) +- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) +- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) +- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) + +*** + +# Editing this README + +When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. + +## Suggestions for a good README +Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. + +## Name +Choose a self-explaining name for your project. + +## Description +Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. + +## Badges +On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. + +## Visuals +Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. + +## Installation +Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. + +## Usage +Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. + +## Support +Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. + +## Roadmap +If you have ideas for releases in the future, it is a good idea to list them in the README. + +## Contributing +State if you are open to contributions and what your requirements are for accepting them. + +For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. + +You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. + +## Authors and acknowledgment +Show your appreciation to those who have contributed to the project. + +## License +For open source projects, say how it is licensed. + +## Project status +If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. diff --git a/kotlin-spring/build.gradle b/kotlin-spring/build.gradle new file mode 100644 index 0000000..c6df720 --- /dev/null +++ b/kotlin-spring/build.gradle @@ -0,0 +1,58 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id 'org.springframework.boot' version '2.7.7' + id 'io.spring.dependency-management' version '1.0.15.RELEASE' + id 'org.jetbrains.kotlin.jvm' version '1.7.21' + id 'org.jetbrains.kotlin.plugin.spring' version '1.7.21' + id 'org.jetbrains.kotlin.plugin.jpa' version '1.7.21' + id 'org.jetbrains.kotlin.plugin.allopen' version '1.7.21' + id 'org.jetbrains.kotlin.kapt' version '1.7.21' +} + +group = 'de.thpeetz.kontor' +version = '0.0.1-SNAPSHOT' +sourceCompatibility = '11' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-mustache' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' + implementation 'org.jetbrains.kotlin:kotlin-reflect' + implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + kapt 'org.springframework.boot:spring-boot-configuration-processor' + runtimeOnly 'com.h2database:h2' + runtimeOnly 'org.springframework.boot:spring-boot-devtools' + testImplementation("org.springframework.boot:spring-boot-starter-test") { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + exclude module: 'mockito-core' + } + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'com.ninja-squad:springmockk:3.1.1' + + +} + +tasks.withType(KotlinCompile) { + kotlinOptions { + freeCompilerArgs = ['-Xjsr305=strict'] + jvmTarget = '11' + } +} + +tasks.named('test') { + useJUnitPlatform() +} + +allOpen { + annotation("javax.persistence.Entity") + annotation("javax.persistence.Embeddable") + annotation("javax.persistence.MappedSuperclass") +} diff --git a/kotlin-spring/gradle/wrapper/gradle-wrapper.jar b/kotlin-spring/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/kotlin-spring/gradlew.bat b/kotlin-spring/gradlew.bat new file mode 100644 index 0000000..53a6b23 --- /dev/null +++ b/kotlin-spring/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kotlin-spring/settings.gradle b/kotlin-spring/settings.gradle new file mode 100644 index 0000000..185aa4e --- /dev/null +++ b/kotlin-spring/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'kontor' diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorApplication.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorApplication.kt new file mode 100644 index 0000000..279b7db --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorApplication.kt @@ -0,0 +1,16 @@ +package de.thpeetz.kontor + +import org.springframework.boot.Banner +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.runApplication + +@SpringBootApplication +@EnableConfigurationProperties(KontorProperties::class) +class KontorApplication + +fun main(args: Array) { + runApplication(*args) { + setBannerMode(Banner.Mode.OFF) + } +} diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorConfiguration.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorConfiguration.kt new file mode 100644 index 0000000..e54f2f9 --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorConfiguration.kt @@ -0,0 +1,79 @@ +package de.thpeetz.kontor + +import de.thpeetz.kontor.comics.* +import org.springframework.boot.ApplicationRunner +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class KontorConfiguration { + @Bean + fun databaseInitializer(artistRepository: ArtistRepository, + publisherRepository: PublisherRepository, + comicRepository: ComicRepository, + issueRepository: IssueRepository + ) = ApplicationRunner { + + artistRepository.save(Artist("Turner, Michael")) + artistRepository.save(Artist("Bendis, Brian Michael")) + artistRepository.save(Artist("Land, Greg")) + artistRepository.save(Artist("Whedon, Joss")) + val marvel = publisherRepository.save(Publisher("Marvel")) + val aspen = publisherRepository.save(Publisher("Aspen")) + publisherRepository.save(Publisher("DC")) + val de = publisherRepository.save(Publisher("Dynamite Entertainment")) + val wildstorm = publisherRepository.save(Publisher("WildStorm")) + val bongo = publisherRepository.save(Publisher("Bongo")) + val image = publisherRepository.save(Publisher("Image")) + val darkHorse = publisherRepository.save(Publisher("Dark Horse Comics")) + val cliffhanger = publisherRepository.save(Publisher("Cliffhanger")) + comicRepository.save(Comic(title = "X-Men", publisher = marvel, currentOrder = false, completed = false)) + val redSonja = comicRepository.save(Comic(title = "Red Sonja", publisher = de, currentOrder = false, completed = false)) + val x23 = comicRepository.save(Comic(title = "X-23", publisher = marvel, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Simpsons Comics", publisher = bongo, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Futurama Comics", publisher = bongo, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Bomb Queen III: The Good, The Bad and The Lovely", publisher = image, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Bomb Queen IV: Suicide Bomber", publisher = image, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Gen13", publisher = wildstorm, currentOrder = false, completed = false)) + val bombqueen = comicRepository.save(Comic(title = "Bomb Queen II: Queen of Hearts", publisher = image, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Iron & The Maiden",publisher = aspen,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Fathom",publisher = aspen,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Soulfire",publisher = aspen,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Star Wars: Rebellion",publisher = darkHorse,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Star Wars: Rebellion",publisher = darkHorse,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Star Wars: Knights of the Old Republic",publisher = darkHorse,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Star Wars: Legacy",publisher = darkHorse,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Star Wars: Dark Times", publisher = darkHorse, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Samurai: Heaven and Earth", publisher = darkHorse, currentOrder = false, completed = false)) + val battlpope = comicRepository.save(Comic(title = "Battle Pope", publisher = image,currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Danger Girl", publisher = cliffhanger, currentOrder = false, completed = false)) + val marville = comicRepository.save(Comic(title = "Marville", publisher = marvel, currentOrder = false, completed = false)) + issueRepository.save(Issue(number = "0", comic = redSonja, gelesen = true)) + issueRepository.save(Issue(number = "1", comic = redSonja, gelesen = true)) + issueRepository.save(Issue(number = "2", comic = redSonja, gelesen = true)) + issueRepository.save(Issue(number = "1", comic = x23, gelesen = false)) + issueRepository.save(Issue(number = "1", comic = bombqueen, gelesen = false)) + issueRepository.save(Issue(number = "2", comic = bombqueen, gelesen = false)) + issueRepository.save(Issue(number = "3", comic = bombqueen, gelesen = false)) + issueRepository.save(Issue(number = "4", comic = bombqueen, gelesen = false)) + issueRepository.save(Issue(number = "1", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "2", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "3", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "4", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "5", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "6", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "7", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "8", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "9", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "10", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "11", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "12", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "1", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "2", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "3", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "4", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "5", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "6", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "7", comic = marville, gelesen = false)) + } +} diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorController.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorController.kt new file mode 100644 index 0000000..3d639dd --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorController.kt @@ -0,0 +1,24 @@ +package de.thpeetz.kontor + +import de.thpeetz.kontor.KontorProperties +import de.thpeetz.kontor.Navigation +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.ui.set +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping + +@Controller +@RequestMapping("/") +class KontorController(val properties: KontorProperties, val navigation: Navigation) { + @GetMapping("/") + fun blog(model: Model): String { + model["title"] = properties.title + model["title"] = "Kontor" + model["banner"] = properties.banner + model["version"] = properties.version + model["navigation"] = navigation.links() + model["login"] = navigation.login() + return "kontor" + } +} \ No newline at end of file diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorProperties.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorProperties.kt new file mode 100644 index 0000000..92c77c7 --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorProperties.kt @@ -0,0 +1,10 @@ +package de.thpeetz.kontor + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.context.properties.ConstructorBinding + +@ConstructorBinding +@ConfigurationProperties("kontor") +data class KontorProperties(var title: String, var version: String = "unknown", val banner: Banner) { + data class Banner(val title: String? = null, val content: String) +} \ No newline at end of file diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/Navigation.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/Navigation.kt new file mode 100644 index 0000000..fb730af --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/Navigation.kt @@ -0,0 +1,24 @@ +package de.thpeetz.kontor + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class Navigation { + + @Bean + fun links(): Iterable = listOf( + Link("/comics", "Comics"), + Link("/library","Library"), + Link("/office", "HomeOffice"), + Link("/tradingcards", "Trading Cards"), + Link("/tysc","TradeYourSportsCards"), + Link("/user/login", ""), + Link("/admin/","Admin") + ) + + @Bean + fun login(): Iterable = listOf(Link("/user/login", "Login")) +} + +data class Link(val link: String, val title: String) diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/RequestLoggingFilter.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/RequestLoggingFilter.kt new file mode 100644 index 0000000..decb70f --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/RequestLoggingFilter.kt @@ -0,0 +1,25 @@ +package de.thpeetz.kontor + +import org.slf4j.LoggerFactory +import javax.servlet.Filter +import org.springframework.stereotype.Component +import javax.servlet.FilterChain +import javax.servlet.ServletRequest +import javax.servlet.ServletResponse + +@Component +class RequestLoggingFilter: Filter { + val loggerFactory = LoggerFactory.getLogger("Kontor Logger") + + override fun doFilter( + request: ServletRequest, + response: ServletResponse, + filterChain: FilterChain + ) { + val requestString = request.servletContext.contextPath.toString() + //val attributeNames = request.attributeNames + //attributeNames.toList().forEach { loggerFactory.info("Attribute: ${it.toString()}") } + loggerFactory.info("Logging request: $requestString") + filterChain.doFilter(request, response) + } +} \ No newline at end of file diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsApiController.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsApiController.kt new file mode 100644 index 0000000..d2e3b4e --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsApiController.kt @@ -0,0 +1,25 @@ +package de.thpeetz.kontor.comics + +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("api/") +class ComicsApiController(val artistRepository: ArtistRepository, + val publisherRepository: PublisherRepository, + val comicRepository: ComicRepository, + val issueRepository: IssueRepository) { + + @GetMapping("/artist") + fun artistList() = artistRepository.findAll() + + @GetMapping("/publisher") + fun publisherList() = publisherRepository.findAll() + + @GetMapping("/comics") + fun comicsList() = comicRepository.findAll() + + @GetMapping("/issues") + fun issuesList() = issueRepository.findAll() +} diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsController.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsController.kt new file mode 100644 index 0000000..f339b04 --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsController.kt @@ -0,0 +1,29 @@ +package de.thpeetz.kontor.comics + +import de.thpeetz.kontor.KontorProperties +import de.thpeetz.kontor.Navigation +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.ui.set +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping + +@Controller +@RequestMapping("/comics") +class ComicsController(private val properties: KontorProperties, + val navigation: Navigation, + val comicRepository: ComicRepository +) { + + @GetMapping("/") + fun blog(model: Model): String { + model["title"] = properties.title + model["title"] = "Comics" + model["banner"] = properties.banner + model["navigation"] = navigation.links() + model["comics"] = comicRepository.findAll().map { it } + return "comics" + } + + +} diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Entities.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Entities.kt new file mode 100644 index 0000000..3be00f5 --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Entities.kt @@ -0,0 +1,36 @@ +package de.thpeetz.kontor.comics + +import javax.persistence.Entity +import javax.persistence.GeneratedValue +import javax.persistence.Id +import javax.persistence.ManyToOne + + +@Entity +class Artist( + val name: String, + @Id @GeneratedValue val id: Long? = null +) + +@Entity +class Publisher( + val name: String, + @Id @GeneratedValue val id: Long? = null +) + +@Entity +class Comic( + val title: String, + @ManyToOne val publisher: Publisher, + val currentOrder: Boolean, + val completed: Boolean, + @Id @GeneratedValue val id: Long? = null +) + +@Entity +class Issue( + val number: String, + @ManyToOne val comic: Comic, + val gelesen: Boolean, + @Id @GeneratedValue val id: Long? = null +) \ No newline at end of file diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Repositories.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Repositories.kt new file mode 100644 index 0000000..fe44b2e --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Repositories.kt @@ -0,0 +1,22 @@ +package de.thpeetz.kontor.comics + +import org.springframework.data.repository.CrudRepository + +interface ArtistRepository: CrudRepository { + + override fun findAll(): Iterable +} + +interface PublisherRepository: CrudRepository { + override fun findAll(): Iterable +} + +interface ComicRepository: CrudRepository { + + override fun findAll(): Iterable +} + +interface IssueRepository: CrudRepository { + + override fun findAll(): Iterable +} \ No newline at end of file diff --git a/kotlin-spring/src/main/resources/application.properties b/kotlin-spring/src/main/resources/application.properties new file mode 100644 index 0000000..6d27d3a --- /dev/null +++ b/kotlin-spring/src/main/resources/application.properties @@ -0,0 +1,6 @@ +spring.jpa.properties.hibernate.globally_quoted_identifiers=true +spring.jpa.properties.hibernate.globally_quoted_identifiers_skip_column_definitions = true +kontor.title=Kontor +kontor.version=1.0.0-SNAPSHOT +kontor.banner.title=Warning +kontor.banner.content=The blog will be down tomorrow. diff --git a/kotlin-spring/src/main/resources/templates/comics.mustache b/kotlin-spring/src/main/resources/templates/comics.mustache new file mode 100644 index 0000000..336d9f0 --- /dev/null +++ b/kotlin-spring/src/main/resources/templates/comics.mustache @@ -0,0 +1,23 @@ +{{> header}} + +{{> menu}} +

+ +
+ + + + {{#comics}} + + {{/comics}} +
List of Comics
Name
{{title}}
+
+ Add entry +
+
+ +{{> footer}} diff --git a/kotlin-spring/src/main/resources/templates/footer.mustache b/kotlin-spring/src/main/resources/templates/footer.mustache new file mode 100644 index 0000000..dde9847 --- /dev/null +++ b/kotlin-spring/src/main/resources/templates/footer.mustache @@ -0,0 +1,22 @@ +
+ + + + diff --git a/kotlin-spring/src/main/resources/templates/header.mustache b/kotlin-spring/src/main/resources/templates/header.mustache new file mode 100644 index 0000000..d794c9e --- /dev/null +++ b/kotlin-spring/src/main/resources/templates/header.mustache @@ -0,0 +1,15 @@ + + + + + {{title}} + + + + + + + + + + diff --git a/kotlin-spring/src/main/resources/templates/kontor.mustache b/kotlin-spring/src/main/resources/templates/kontor.mustache new file mode 100644 index 0000000..177306f --- /dev/null +++ b/kotlin-spring/src/main/resources/templates/kontor.mustache @@ -0,0 +1,18 @@ +{{> header}} + +{{> menu}} + +
+ {{#banner.title}} +
+ + +
+ {{/banner.title}} +
+ +{{> footer}} diff --git a/kotlin-spring/src/main/resources/templates/menu.mustache b/kotlin-spring/src/main/resources/templates/menu.mustache new file mode 100644 index 0000000..c4d9ba0 --- /dev/null +++ b/kotlin-spring/src/main/resources/templates/menu.mustache @@ -0,0 +1,23 @@ + diff --git a/kotlin-spring/src/test/resources/junit-platform.properties b/kotlin-spring/src/test/resources/junit-platform.properties new file mode 100644 index 0000000..ab79697 --- /dev/null +++ b/kotlin-spring/src/test/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.testinstance.lifecycle.default = per_class diff --git a/wicket/.github/workflows/gradle.yml b/wicket/.github/workflows/gradle.yml new file mode 100644 index 0000000..58e1c59 --- /dev/null +++ b/wicket/.github/workflows/gradle.yml @@ -0,0 +1,26 @@ +# This workflow will build a Java project with Gradle +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Java CI with Gradle + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build diff --git a/wicket/.gitignore b/wicket/.gitignore new file mode 100644 index 0000000..2e1f2c3 --- /dev/null +++ b/wicket/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +bin/ +.classpath +.project +.settings/ +.asciidoctorconfig.adoc diff --git a/wicket/.gitlab-ci.yml b/wicket/.gitlab-ci.yml new file mode 100644 index 0000000..514cefd --- /dev/null +++ b/wicket/.gitlab-ci.yml @@ -0,0 +1,36 @@ +variables: + GRADLE_OPTS: "-Dorg.gradle.daemon=false" + +before_script: + - source "/home/gitlab-runner/.sdkman/bin/sdkman-init.sh" + - sdk u java 11.0.12-open + +stages: + - build + - test + - analysis + - publish + +Build Application: + stage: build + script: ./gradlew build + +Create Documentation: + stage: build + script: ./gradlew asciidoctorPdf + +Test Application: + stage: test + script: ./gradlew check + +sonarqube-check: + stage: analysis + variables: + SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar" # Defines the location of the analysis task cache + GIT_DEPTH: "0" # Tells git to fetch all the branches of the project, required by the analysis task + script: ./gradlew sonarqube + allow_failure: true + +Publish Artifacts: + stage: publish + script: ./gradlew publish diff --git a/wicket/README.md b/wicket/README.md new file mode 100644 index 0000000..ab12d07 --- /dev/null +++ b/wicket/README.md @@ -0,0 +1,4 @@ +![Java CI with Gradle](https://github.com/tpeetz/kontor-wicket/workflows/Java%20CI%20with%20Gradle/badge.svg) + +# kontor-wicket +Kontor Application with Apache Wicket diff --git a/wicket/build.gradle b/wicket/build.gradle new file mode 100644 index 0000000..0afbe9f --- /dev/null +++ b/wicket/build.gradle @@ -0,0 +1,61 @@ +plugins { + id 'war' + id 'jacoco-report-aggregation' + alias(versions.plugins.asciidoctorConvention) + alias(versions.plugins.javaConvention) + alias(versions.plugins.sonarqube) +} + +final BUILD_DATE = new Date().format('dd.MM.yyyy').toString() + +dependencies { + implementation versions.slf4j + implementation versions.commonscli + testImplementation versions.junit + implementation versions.bundles.logback + spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.12.0' + + implementation 'org.apache.wicket:wicket:9.9.1' + implementation 'org.apache.wicket:wicket-extensions:9.9.1' + testImplementation 'org.eclipse.jetty:jetty-webapp:9.4.44.v20210927' + testImplementation 'org.eclipse.jetty:jetty-jmx:9.4.44.v20210927' +} + +publishing { + publications { + application(MavenPublication) { + groupId = group + from components.java + } + } +} + +jacocoTestReport { + reports { + xml.enabled true + } +} + +test.finalizedBy jacocoTestReport + +spotbugs { + ignoreFailures = true +} + +sonarqube { + properties { + property "sonar.projectKey", "kontor_kontor-wicket_AYCAQ47WRCd9N673sSSD" + property "sonar.host.url", "https://sonar.thpeetz.de" + property "sonar.login", "7cf604ab1ebf48f7dc60d942c8196a132b228a6d" + property "sonar.qualitygate.wait", true + property "sonar.sourceEncoding", "UTF-8" + } +} + +tasks.named('sonarqube').configure { + dependsOn test +} + +wrapper { + gradleVersion = "7.5" +} diff --git a/wicket/gradle.properties b/wicket/gradle.properties new file mode 100644 index 0000000..e140eca --- /dev/null +++ b/wicket/gradle.properties @@ -0,0 +1,3 @@ +description='Anwendung Kontor mit Apache Wicket' +group=de.thpeetz +version=1.0.0-SNAPSHOT diff --git a/wicket/gradle/wrapper/gradle-wrapper.jar b/wicket/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/wicket/gradlew.bat b/wicket/gradlew.bat new file mode 100644 index 0000000..53a6b23 --- /dev/null +++ b/wicket/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/wicket/settings.gradle b/wicket/settings.gradle new file mode 100644 index 0000000..f66ecb4 --- /dev/null +++ b/wicket/settings.gradle @@ -0,0 +1 @@ +rootProject.name = "kontor-wicket" diff --git a/wicket/src/docs/asciidoc/kontor-wicket.adoc b/wicket/src/docs/asciidoc/kontor-wicket.adoc new file mode 100644 index 0000000..e310b6f --- /dev/null +++ b/wicket/src/docs/asciidoc/kontor-wicket.adoc @@ -0,0 +1,179 @@ += Projektbeschreibung kontor-wicket: Entwicklungs- und Projekthandbuch +:author: Thomas Peetz +:email: +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: +:toclevels: 4 +:table-caption!: +:counter: table-number: 0 + +[title="Dokumenthistorie", id="Table-{counter:table-number}", options="header"] +|=== +| Version | Datum | Autor | Änderungsgrund / Bemerkungen +| 1.0.0 | 16.05.2022 | Thomas Peetz | Ersterstellung +|=== + +== Allgemeines + +=== Zweck des Dokumentes + +Das Entwicklungshandbuch beschreibt die Werkzeuge und die Vorgehensweise bei der Entwicklung +im Projekt kontor-wicket und der Erstellung der Dokumentation. + +=== Verwendete Tools + +==== Gitlab + +Für die Verwaltung des Sourcecode kommt ((Gitlab))<> zum Einsatz. +Mit Gitlab werden auch die Projektaufgaben verwaltet. + +Das Projekt und das dazugehörige Git Repository sind unter der Adresse + +https://gitlab.ingenieurbuero-peetz.de/kontor/kontor-wicket + +zu finden. + +== Erstellung der Dokumentation + +Die Dokumentation des Projektes wird mit ((Asciidoctor))<> geschrieben. +Die Dokumente erhalten ihre Namen nach dem jeweiligen Hauptdokument. + +=== Quellcode Verwaltung + +Die Asciidoctor-Dateien haben die Endung `.adoc`. + +=== Buildsystem + +Zur Erstellung der PDF-Dateien aus den Asciidoctor-Dateien wird das Buildsystem ((Gradle))<<3>> verwendet. +Die Dateien für die Dokumente liegen im Verzeichnis `src/docs/asciidoc`. + +Der Gradle Build wird über die Datei `build.gradle` definiert. + + +== Einführung + +=== Zweck + +=== Stakeholder des Systems + +=== Systemumfang + +==== Zielsetzung des Systems + +=== Systemübersicht + +==== Systemkontext + +==== Systemarchitektur + +==== Systemschnittstellen + +===== Realisierte Schnittstellen + +===== Verwendete Schnittstellen + +==== Logisches Datenmodell + +==== Einschränkungen + +== Anforderungen der Domäne + +=== Systemfunktionen + +==== Anwendungsfälle + +==== Akteure + +==== Zielgruppen + +=== Anforderungen + +==== Anforderungen an externe Schnittstellen + +==== Funktionale Anforderungen + +==== Qualitätsanforderungen + +==== Randbedingungen + +==== Weitere Anforderungen + +==== Wartungs- und Supportinformationen + +=== Verifikation + +== Projektbeschreibung + +=== Ausgangslage + +//==== Rechtliche Vorgaben und Rahmenbedingungen +//=== Rahmenbedingungen + +//==== Vorhandene Regelungen + +=== Projektziele + +=== Projektabgrenzung + +//=== Voraussichtliche Kosten + +//=== Projektrisiken + +//==== Produktivität + +//==== Finanzielle Risiken + +//==== Akzeptanz + +== Projektorganisation + +=== Projekt-Aufbauorganisation + +=== Rollendefinition + +//==== Projektauftraggeber + +//==== Projektausschuss + +//==== Beratung / Qualitätssicherung + +==== Projekteiter + +==== Projektteam + +==== Liste der Stakeholder + +=== Projektablauforganisation + +==== Projekt-Phasen + +===== Erstellung der Projektdokumentation + + +== Verschiedenes + +=== Erreichbarkeiten + +[bibliography] +== Referenzen + +- [[[asciidoctor]]] http://asciidoctor.org +- [[[gitlab]]] http://www.gitlab.org +- [[[gradle]]] http://www.gradle.org +- [[[jenkins]]] http://jenkins-ci.org + +[glossary] +== Glossar + +[index] +== Index + +== Verzeichnisse + +=== Abbildungsverzeichnis + +=== Tabellenverzeichnis + +<> <> diff --git a/wicket/src/main/java/de/thpeetz/kontor/HomePage.java b/wicket/src/main/java/de/thpeetz/kontor/HomePage.java new file mode 100644 index 0000000..e5a3c82 --- /dev/null +++ b/wicket/src/main/java/de/thpeetz/kontor/HomePage.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor; + +import org.apache.wicket.request.mapper.parameter.PageParameters; +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.markup.html.WebPage; + +public class HomePage extends WebPage { + private static final long serialVersionUID = 1L; + + public HomePage(final PageParameters parameters) { + super(parameters); + + add(new Label("version", getApplication().getFrameworkSettings().getVersion())); + + // TODO Add your page's components here + + } +} diff --git a/wicket/src/main/java/de/thpeetz/kontor/KontorApplication.java b/wicket/src/main/java/de/thpeetz/kontor/KontorApplication.java new file mode 100644 index 0000000..9c39858 --- /dev/null +++ b/wicket/src/main/java/de/thpeetz/kontor/KontorApplication.java @@ -0,0 +1,41 @@ +package de.thpeetz.kontor; + +import org.apache.wicket.csp.CSPDirective; +import org.apache.wicket.csp.CSPDirectiveSrcValue; +import org.apache.wicket.markup.html.WebPage; +import org.apache.wicket.protocol.http.WebApplication; + +/** + * Application object for your web application. + * If you want to run this application without deploying, run the Start class. + * + * @see de.thpeetz.kontor.Start#main(String[]) + */ +public class KontorApplication extends WebApplication +{ + /** + * @see org.apache.wicket.Application#getHomePage() + */ + @Override + public Class getHomePage() + { + return HomePage.class; + } + + /** + * @see org.apache.wicket.Application#init() + */ + @Override + public void init() + { + super.init(); + + // needed for the styling used by the quickstart + getCspSettings().blocking() + .add(CSPDirective.STYLE_SRC, CSPDirectiveSrcValue.SELF) + .add(CSPDirective.STYLE_SRC, "https://fonts.googleapis.com/css") + .add(CSPDirective.FONT_SRC, "https://fonts.gstatic.com"); + + // add your configuration here + } +} diff --git a/wicket/src/main/resources/de/thpeetz/kontor/HomePage.html b/wicket/src/main/resources/de/thpeetz/kontor/HomePage.html new file mode 100644 index 0000000..4ca32fc --- /dev/null +++ b/wicket/src/main/resources/de/thpeetz/kontor/HomePage.html @@ -0,0 +1,62 @@ + + + + + Apache Wicket Quickstart + + + + +

+ +
+
+

Congratulations!

+

+ Your quick start works! This project is especially useful to + start developing your Wicket application or to create a test + case for a bug report. +

+

Get started

+

+ You can even switch to HTTPS! +

+

+ From here you can start hacking away at your application and + wow your clients: +

+ +

Get help

+

+ We are here to help! +

+ +

Reporting a bug

+

+ Help us help you: +

+
    +
  1. reproduce the bug with the least amount of code
  2. +
  3. create a unit test that shows the bug
  4. +
  5. fix the bug and create a patch
  6. +
  7. attach the result of step 1, 2 or 3 to a JIRA issue
  8. +
  9. profit!
  10. +
+

+ Please mention the correct Wicket version: 1.5-SNAPSHOT. +

+
+
+
+ + diff --git a/wicket/src/main/webapp/WEB-INF/web.xml b/wicket/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..f5b288e --- /dev/null +++ b/wicket/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,32 @@ + + + + kontor-wicket + + + + + wicket.kontor-wicket + org.apache.wicket.protocol.http.WicketFilter + + applicationClassName + de.thpeetz.kontor.KontorApplication + + + + + wicket.kontor-wicket + /* + + diff --git a/wicket/src/main/webapp/logo.png b/wicket/src/main/webapp/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..39ec54854b748ab6aeb6b3965d88f452772d7d8e GIT binary patch literal 12244 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 i1O^!^0gjqN1OFf9qq%!F8)!QK0000`K literal 0 HcmV?d00001 diff --git a/wicket/src/main/webapp/style.css b/wicket/src/main/webapp/style.css new file mode 100644 index 0000000..87576a7 --- /dev/null +++ b/wicket/src/main/webapp/style.css @@ -0,0 +1,68 @@ +body, p, li, a { font-family: georgia, times, serif;font-size:13pt;} +h1, h2, h3 { font-family: 'Yanone Kaffeesatz', arial, serif; } +body { margin:0;padding:0;} +#hd { + width : 100%; + height : 87px; + background-color : #092E67; + margin-top : 0; + padding-top : 10px; + border-bottom : 1px solid #888; + z-index : 0; +} +#ft { + position : absolute; + bottom : 0; + width : 100%; + height : 99px; + background-color : #6493D2; + border-top : 1px solid #888; + z-index : 0; +} +#logo,#bd { + width : 650px; + margin: 0 auto; + padding: 25px 50px 0 50px; +} +#logo h1 { + color : white; + font-size:36pt; + display: inline; +} +#logo img { + display:inline; + vertical-align: bottom; + margin-left : 50px; + margin-right : 5px; +} +body { margin-top : 0; padding-top : 0;} +#logo, #logo h1 { margin-top : 0; padding-top : 0;} +#bd { + position : absolute; + top : 75px; + bottom : 75px; + left : 50%; + margin-left : -325px; + z-index : 1; + overflow: auto; + background-color : #fff; + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; + -moz-box-shadow: 0px 0px 10px #888; + -webkit-box-shadow: 0px 0px 10px #888; + box-shadow: 0px 0px 10px #888; +} +a, a:visited, a:hover, a:active { + color : #6493D2; +} +h2 { + padding : 0; margin:0; + font-size:36pt; + color:#FF5500; +} +h3 { + padding : 0; margin:0; + font-size:24pt; + color:#092E67; +} diff --git a/wicket/src/test/java/de/thpeetz/kontor/Start.java b/wicket/src/test/java/de/thpeetz/kontor/Start.java new file mode 100644 index 0000000..49c740a --- /dev/null +++ b/wicket/src/test/java/de/thpeetz/kontor/Start.java @@ -0,0 +1,106 @@ +package de.thpeetz.kontor; + +import java.lang.management.ManagementFactory; + +import javax.management.MBeanServer; + +import org.eclipse.jetty.jmx.MBeanContainer; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.webapp.WebAppContext; + +/** + * Separate startup class for people that want to run the examples directly. Use parameter + * -Dcom.sun.management.jmxremote to startup JMX (and e.g. connect with jconsole). + */ +public class Start +{ + /** + * Main function, starts the jetty server. + * + * @param args + */ + public static void main(String[] args) + { + System.setProperty("wicket.configuration", "development"); + + Server server = new Server(); + + HttpConfiguration http_config = new HttpConfiguration(); + http_config.setSecureScheme("https"); + http_config.setSecurePort(8443); + http_config.setOutputBufferSize(32768); + + ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(http_config)); + http.setPort(8080); + http.setIdleTimeout(1000 * 60 * 60); + + server.addConnector(http); + + Resource keystore = Resource.newClassPathResource("/keystore.p12"); + if (keystore != null && keystore.exists()) + { + // if a keystore for a SSL certificate is available, start a SSL + // connector on port 8443. + // By default, the quickstart comes with a Apache Wicket Quickstart + // Certificate that expires about half way september 2031. Do not + // use this certificate anywhere important as the passwords are + // available in the source. + + SslContextFactory sslContextFactory = new SslContextFactory(); + sslContextFactory.setKeyStoreResource(keystore); + sslContextFactory.setKeyStorePassword("wicket"); + sslContextFactory.setKeyManagerPassword("wicket"); + + HttpConfiguration https_config = new HttpConfiguration(http_config); + https_config.addCustomizer(new SecureRequestCustomizer()); + + ServerConnector https = new ServerConnector(server, new SslConnectionFactory( + sslContextFactory, "http/1.1"), new HttpConnectionFactory(https_config)); + https.setPort(8443); + https.setIdleTimeout(500000); + + server.addConnector(https); + System.out.println("SSL access to the examples has been enabled on port 8443"); + System.out + .println("You can access the application using SSL on https://localhost:8443"); + System.out.println(); + } + + WebAppContext bb = new WebAppContext(); + bb.setServer(server); + bb.setContextPath("/"); + bb.setWar("src/main/webapp"); + + // uncomment the next two lines if you want to start Jetty with WebSocket (JSR-356) support + // you need org.apache.wicket:wicket-native-websocket-javax in the classpath! + // ServerContainer serverContainer = WebSocketServerContainerInitializer.configureContext(bb); + // serverContainer.addEndpoint(new WicketServerEndpointConfig()); + + // uncomment next line if you want to test with JSESSIONID encoded in the urls + // ((AbstractSessionManager) + // bb.getSessionHandler().getSessionManager()).setUsingCookies(false); + + server.setHandler(bb); + + MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); + MBeanContainer mBeanContainer = new MBeanContainer(mBeanServer); + server.addEventListener(mBeanContainer); + server.addBean(mBeanContainer); + + try + { + server.start(); + server.join(); + } catch (Exception e) { + e.printStackTrace(); + System.exit(100); + } + } +} diff --git a/wicket/src/test/java/de/thpeetz/kontor/TestHomePage.java b/wicket/src/test/java/de/thpeetz/kontor/TestHomePage.java new file mode 100644 index 0000000..fe9c409 --- /dev/null +++ b/wicket/src/test/java/de/thpeetz/kontor/TestHomePage.java @@ -0,0 +1,29 @@ +package de.thpeetz.kontor; + +import org.apache.wicket.util.tester.WicketTester; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Simple test using the WicketTester + */ +public class TestHomePage +{ + private WicketTester tester; + + @BeforeEach + public void setUp() + { + tester = new WicketTester(new KontorApplication()); + } + + @Test + public void homepageRendersSuccessfully() + { + //start and render the test page + tester.startPage(HomePage.class); + + //assert rendered page class + tester.assertRenderedPage(HomePage.class); + } +} diff --git a/wicket/src/test/resources/jetty/jetty-http.xml b/wicket/src/test/resources/jetty/jetty-http.xml new file mode 100644 index 0000000..7b39acb --- /dev/null +++ b/wicket/src/test/resources/jetty/jetty-http.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wicket/src/test/resources/jetty/jetty-https.xml b/wicket/src/test/resources/jetty/jetty-https.xml new file mode 100644 index 0000000..35100e7 --- /dev/null +++ b/wicket/src/test/resources/jetty/jetty-https.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + http/1.1 + + + + + + + + + + + + + 30000 + + + + diff --git a/wicket/src/test/resources/jetty/jetty-ssl.xml b/wicket/src/test/resources/jetty/jetty-ssl.xml new file mode 100644 index 0000000..f23231b --- /dev/null +++ b/wicket/src/test/resources/jetty/jetty-ssl.xml @@ -0,0 +1,57 @@ + + + + + + + + + / + + + + + + SSL_RSA_WITH_DES_CBC_SHA + SSL_DHE_RSA_WITH_DES_CBC_SHA + SSL_DHE_DSS_WITH_DES_CBC_SHA + SSL_RSA_EXPORT_WITH_RC4_40_MD5 + SSL_RSA_EXPORT_WITH_DES40_CBC_SHA + SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA + SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA + TLS_RSA_WITH_AES_256_GCM_SHA384 + TLS_RSA_WITH_AES_128_GCM_SHA256 + TLS_RSA_WITH_AES_256_CBC_SHA256 + TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA + TLS_RSA_WITH_AES_256_CBC_SHA + TLS_RSA_WITH_AES_256_CBC_SHA + TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA + TLS_ECDH_RSA_WITH_AES_256_CBC_SHA + TLS_DHE_RSA_WITH_AES_256_CBC_SHA + TLS_DHE_DSS_WITH_AES_256_CBC_SHA + TLS_RSA_WITH_AES_128_CBC_SHA256 + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA + TLS_RSA_WITH_AES_128_CBC_SHA + TLS_RSA_WITH_AES_128_CBC_SHA + TLS_RSA_WITH_AES_128_CBC_SHA + TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA + TLS_ECDH_RSA_WITH_AES_128_CBC_SHA + TLS_DHE_RSA_WITH_AES_128_CBC_SHA + TLS_DHE_DSS_WITH_AES_128_CBC_SHA + + + + + + + + + + + + + + + diff --git a/wicket/src/test/resources/jetty/jetty.xml b/wicket/src/test/resources/jetty/jetty.xml new file mode 100644 index 0000000..5590715 --- /dev/null +++ b/wicket/src/test/resources/jetty/jetty.xml @@ -0,0 +1,23 @@ + + + + + + + + https + + + + 32768 + 8192 + 8192 + true + false + 512 + + + + + + diff --git a/wicket/src/test/resources/keystore b/wicket/src/test/resources/keystore new file mode 100644 index 0000000000000000000000000000000000000000..1473db33287b4de9be58dc79741ac9d5c088919a GIT binary patch literal 3954 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 e{K-i+ON5SRjUU_1uu~kDP_^p8r(#`g+`j Date: Thu, 9 Jan 2025 22:28:46 +0100 Subject: [PATCH 37/91] add repo kontor-quarkus --- java-quarkus/.gitignore | 9 + java-quarkus/.gitlab-ci.yml | 38 +++ java-quarkus/README.md | 60 ++++ java-quarkus/build.gradle | 158 ++++++++++ java-quarkus/config/checkstyle/checkstyle.xml | 14 + java-quarkus/gradle.properties | 9 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 58910 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + java-quarkus/gradlew | 185 ++++++++++++ java-quarkus/gradlew.bat | 104 +++++++ java-quarkus/settings.gradle | 11 + .../src/docs/asciidoc/kontor-quarkus.adoc | 84 ++++++ java-quarkus/src/main/docker/Dockerfile.jvm | 94 ++++++ .../src/main/docker/Dockerfile.legacy-jar | 90 ++++++ .../src/main/docker/Dockerfile.native | 27 ++ .../src/main/docker/Dockerfile.native-micro | 30 ++ .../thpeetz/kontor/comics/ComicsResource.java | 95 ++++++ .../kontor/comics/service/ArtistService.java | 28 ++ .../kontor/comics/service/ComicsService.java | 12 + .../comics/service/PublisherService.java | 28 ++ .../resources/META-INF/resources/index.html | 284 ++++++++++++++++++ .../src/main/resources/application.properties | 3 + .../kontor/comics/ComicsResourceIT.java | 8 + .../kontor/comics/ComicsResourceTest.java | 19 ++ 24 files changed, 1395 insertions(+) create mode 100644 java-quarkus/.gitignore create mode 100644 java-quarkus/.gitlab-ci.yml create mode 100644 java-quarkus/README.md create mode 100644 java-quarkus/build.gradle create mode 100644 java-quarkus/config/checkstyle/checkstyle.xml create mode 100644 java-quarkus/gradle.properties create mode 100644 java-quarkus/gradle/wrapper/gradle-wrapper.jar create mode 100644 java-quarkus/gradle/wrapper/gradle-wrapper.properties create mode 100644 java-quarkus/gradlew create mode 100644 java-quarkus/gradlew.bat create mode 100644 java-quarkus/settings.gradle create mode 100644 java-quarkus/src/docs/asciidoc/kontor-quarkus.adoc create mode 100644 java-quarkus/src/main/docker/Dockerfile.jvm create mode 100644 java-quarkus/src/main/docker/Dockerfile.legacy-jar create mode 100644 java-quarkus/src/main/docker/Dockerfile.native create mode 100644 java-quarkus/src/main/docker/Dockerfile.native-micro create mode 100644 java-quarkus/src/main/java/de/thpeetz/kontor/comics/ComicsResource.java create mode 100644 java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ArtistService.java create mode 100644 java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ComicsService.java create mode 100644 java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/PublisherService.java create mode 100644 java-quarkus/src/main/resources/META-INF/resources/index.html create mode 100644 java-quarkus/src/main/resources/application.properties create mode 100644 java-quarkus/src/native-test/java/de/thpeetz/kontor/comics/ComicsResourceIT.java create mode 100644 java-quarkus/src/test/java/de/thpeetz/kontor/comics/ComicsResourceTest.java diff --git a/java-quarkus/.gitignore b/java-quarkus/.gitignore new file mode 100644 index 0000000..cec4d77 --- /dev/null +++ b/java-quarkus/.gitignore @@ -0,0 +1,9 @@ +build/ +.settings/ +.gradle/ +.classpath +.project +.vscode/ +bin/ +.idea/ +/.eclipse-pmd diff --git a/java-quarkus/.gitlab-ci.yml b/java-quarkus/.gitlab-ci.yml new file mode 100644 index 0000000..ed4caf6 --- /dev/null +++ b/java-quarkus/.gitlab-ci.yml @@ -0,0 +1,38 @@ +variables: + GRADLE_OPTS: "-Dorg.gradle.daemon=false" + +before_script: + - source "/home/gitlab-runner/.sdkman/bin/sdkman-init.sh" + - sdk u java 11.0.12-open + - chmod +x ./gradlew + +stages: + - build + - test + - analysis + - publish + +Build Application: + stage: build + script: ./gradlew --build-cache compileJava + +Create Documentation: + stage: build + script: ./gradlew asciidoctorPdf + +Test Application: + stage: test + script: ./gradlew check + +sonarqube-check: + stage: analysis + variables: + SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar" # Defines the location of the analysis task cache + GIT_DEPTH: "0" # Tells git to fetch all the branches of the project, required by the analysis task + script: ./gradlew sonarqube + allow_failure: true + +Publish Artifacts: + stage: publish + script: ./gradlew publish + diff --git a/java-quarkus/README.md b/java-quarkus/README.md new file mode 100644 index 0000000..11ec472 --- /dev/null +++ b/java-quarkus/README.md @@ -0,0 +1,60 @@ +# kontor-quarkus Project + +This project uses Quarkus, the Supersonic Subatomic Java Framework. + +If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ . + +## Running the application in dev mode + +You can run your application in dev mode that enables live coding using: +```shell script +./gradlew quarkusDev +``` + +> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. + +## Packaging and running the application + +The application can be packaged using: +```shell script +./gradlew build +``` +It produces the `quarkus-run.jar` file in the `build/quarkus-app/` directory. +Be aware that it’s not an _über-jar_ as the dependencies are copied into the `build/quarkus-app/lib/` directory. + +The application is now runnable using `java -jar build/quarkus-app/quarkus-run.jar`. + +If you want to build an _über-jar_, execute the following command: +```shell script +./gradlew build -Dquarkus.package.type=uber-jar +``` + +The application, packaged as an _über-jar_, is now runnable using `java -jar build/*-runner.jar`. + +## Creating a native executable + +You can create a native executable using: +```shell script +./gradlew build -Dquarkus.package.type=native +``` + +Or, if you don't have GraalVM installed, you can run the native executable build in a container using: +```shell script +./gradlew build -Dquarkus.package.type=native -Dquarkus.native.container-build=true +``` + +You can then execute your native executable with: `./build/kontor-quarkus-1.0.0-SNAPSHOT-runner` + +If you want to learn more about building native executables, please consult https://quarkus.io/guides/gradle-tooling. + +## Related Guides + +- RESTEasy Reactive ([guide](https://quarkus.io/guides/resteasy-reactive)): A JAX-RS implementation utilizing build time processing and Vert.x. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it. + +## Provided Code + +### RESTEasy Reactive + +Easily start your Reactive RESTful Web Services + +[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources) diff --git a/java-quarkus/build.gradle b/java-quarkus/build.gradle new file mode 100644 index 0000000..57e8102 --- /dev/null +++ b/java-quarkus/build.gradle @@ -0,0 +1,158 @@ +plugins { + id 'java' + id 'maven-publish' + id 'jacoco' + id 'jacoco-report-aggregation' + id 'pmd' + id 'checkstyle' + id 'jvm-test-suite' + id 'io.quarkus' + id "org.sonarqube" version "3.3" + alias(versionsLibs.plugins.asciidoctorConvert) + alias(versionsLibs.plugins.asciidoctorPdf) + alias(versionsLibs.plugins.asciidoctorGems) +} + +repositories { + ruby.gems() + maven { + url "https://nexus.thpeetz.de/nexus/content/groups/public/" + } + mavenCentral() + mavenLocal() +} + +dependencies { + implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") + implementation 'io.quarkus:quarkus-resteasy-reactive' + implementation 'io.quarkus:quarkus-resteasy-reactive-jackson' + implementation 'io.quarkus:quarkus-arc' + testImplementation 'io.quarkus:quarkus-junit5' + testImplementation 'io.rest-assured:rest-assured' + testImplementation("io.quarkus:quarkus-jacoco") +} + +final BUILD_DATE = new Date().format('dd.MM.yyyy').toString() +def pdfFile = layout.buildDirectory.file("docs/asciidocPdf/" + project.name + ".pdf") +def pdfArtifact = artifacts.add('archives', pdfFile.get().asFile) { + type 'pdf' + builtBy asciidoctorPdf +} + +tasks.withType(GenerateModuleMetadata).configureEach { + suppressedValidationErrors.add('enforced-platform') +} + +publishing { + publications { + quarkusApp(MavenPublication){ + from components.java + } + docs(MavenPublication) { + groupId = group + '.docs' + artifactId = project.name + artifact pdfArtifact + } + } + repositories { + maven { + name = 'nexusPeetz' + def releasesRepoUrl = "https://nexus.thpeetz.de/nexus/content/repositories/releases/" + def snapshotsRepoUrl = "https://nexus.thpeetz.de/nexus/content/repositories/snapshots/" + url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl + credentials(PasswordCredentials) + } + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +tasks.withType(GenerateModuleMetadata).configureEach { + suppressedValidationErrors.add('enforced-platform') +} + +compileJava { + options.encoding = 'UTF-8' + options.compilerArgs << '-parameters' +} + +compileTestJava { + options.encoding = 'UTF-8' +} + +jacocoTestReport { + reports { + xml.required = true + } +} + +test.finalizedBy jacocoTestReport + + +pmd { + //ruleSetFiles = files("custom-pmd-ruleset.xml") + ruleSets = ["category/java/errorprone.xml", "category/java/bestpractices.xml"] + ignoreFailures = true +} + +tasks.withType(Checkstyle) { + reports { + xml.required = true + html.required = true + } +} + +testing { + suites { + test { + useJUnitJupiter() + } + } +} + +pmdTest.dependsOn("compileQuarkusTestGeneratedSourcesJava") +checkstyleTest.dependsOn("compileQuarkusTestGeneratedSourcesJava") +jacocoTestReport.dependsOn("compileQuarkusGeneratedSourcesJava") +pmdMain.dependsOn("compileQuarkusGeneratedSourcesJava") + +sonarqube { + properties { + property "sonar.host.url", "https://sonar.thpeetz.de" + property "sonar.qualitygate.wait", true + property "sonar.sourceEncoding", "UTF-8" + property "sonar.projectKey", "kontor_kontor-quarkus_AYQek8Mxz0hBjLSV8I8O" + property "sonar.login", "5ecd90dee57806857e07443a9b0efd3cd7774a81" + } +} + +tasks.named('sonarqube').configure { + dependsOn 'test', 'pmdMain', 'checkstyleMain', 'jacocoTestReport' +} + +asciidoctorPdf { + baseDirFollowsSourceFile() + + asciidoctorj { + attributes 'build-gradle': file('build.gradle'), + 'endpoint-url': 'https://www.thpeetz.de', + 'source-highlighter': 'rouge', + 'imagesdir': './images', + 'toc': 'left', + 'toc-title': 'Inhaltsverzeichnis', + 'revdate': BUILD_DATE, + 'revnumber': { project.version.toString() }, + 'revremark': 'Entwurf', + 'chapter-label': '', + 'icons': 'font', + 'idprefix': 'id_', + 'idseparator': '-', + 'docinfo1': '' + } +} + +wrapper { + gradleVersion = "7.5" +} diff --git a/java-quarkus/config/checkstyle/checkstyle.xml b/java-quarkus/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..2574b06 --- /dev/null +++ b/java-quarkus/config/checkstyle/checkstyle.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java-quarkus/gradle.properties b/java-quarkus/gradle.properties new file mode 100644 index 0000000..c30b39f --- /dev/null +++ b/java-quarkus/gradle.properties @@ -0,0 +1,9 @@ +#Gradle properties +group=de.thpeetz.kontor +version=1.0.0-SNAPSHOT + +quarkusPluginId=io.quarkus +quarkusPluginVersion=2.13.2.Final +quarkusPlatformGroupId=io.quarkus.platform +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformVersion=2.13.2.Final \ No newline at end of file diff --git a/java-quarkus/gradle/wrapper/gradle-wrapper.jar b/java-quarkus/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..62d4c053550b91381bbd28b1afc82d634bf73a8a GIT binary patch literal 58910 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 literal 0 HcmV?d00001 diff --git a/java-quarkus/gradle/wrapper/gradle-wrapper.properties b/java-quarkus/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ae04661 --- /dev/null +++ b/java-quarkus/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/java-quarkus/gradlew b/java-quarkus/gradlew new file mode 100644 index 0000000..fbd7c51 --- /dev/null +++ b/java-quarkus/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/java-quarkus/gradlew.bat b/java-quarkus/gradlew.bat new file mode 100644 index 0000000..a9f778a --- /dev/null +++ b/java-quarkus/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/java-quarkus/settings.gradle b/java-quarkus/settings.gradle new file mode 100644 index 0000000..0ab2682 --- /dev/null +++ b/java-quarkus/settings.gradle @@ -0,0 +1,11 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + mavenLocal() + } + plugins { + id "${quarkusPluginId}" version "${quarkusPluginVersion}" + } +} +rootProject.name='kontor-quarkus' diff --git a/java-quarkus/src/docs/asciidoc/kontor-quarkus.adoc b/java-quarkus/src/docs/asciidoc/kontor-quarkus.adoc new file mode 100644 index 0000000..d5d7ac4 --- /dev/null +++ b/java-quarkus/src/docs/asciidoc/kontor-quarkus.adoc @@ -0,0 +1,84 @@ += Projektbeschreibung kontor-quarkus: Pflichtenheft und Projekthandbuch +:author: Thomas Peetz +:email: +:doctype: article +:sectnums: +:sectnumlevels: 4 +:toc: +:toclevels: 4 +:table-caption!: +:counter: table-number: 0 + +//[title="Dokumenthistorie", caption="Tabelle {counter:table-number} ", id="Tabelle-{counter:table-number}", options="header"] +//[title="Dokumenthistorie", id="Table-{counter:table-number}", options="header", cols="4"] +[title="Dokumenthistorie", id="Table-{counter:table-number}", options="header"] +|=== +| Version | Datum | Autor | Änderungsgrund / Bemerkungen +| 0.0.1 | 16.05.2022 | Thomas Peetz | Ersterstellung +|=== + +== Einführung + +=== Zweck + +=== Stakeholder des Systems + +=== Systemumfang + +==== Zielsetzung des Systems + +=== Systemübersicht + +==== Systemkontext + +==== Systemarchitektur + +==== Systemschnittstellen + +===== Realisierte Schnittstellen + +===== Verwendete Schnittstellen + +==== Logisches Datenmodell + +==== Einschränkungen + +== Anforderungen der Domäne + +=== Systemfunktionen + +==== Anwendungsfälle + +==== Akteure + +==== Zielgruppen + +=== Anforderungen + +==== Anforderungen an externe Schnittstellen + +==== Funktionale Anforderungen + +==== Qualitätsanforderungen + +==== Randbedingungen + +==== Weitere Anforderungen + +==== Wartungs- und Supportinformationen + +=== Verifikation + +[bibliography] +== Referenzen + +[glossary] +== Glossar + +== Verzeichnisse + +=== Abbildungsverzeichnis + +=== Tabellenverzeichnis + +<> <> diff --git a/java-quarkus/src/main/docker/Dockerfile.jvm b/java-quarkus/src/main/docker/Dockerfile.jvm new file mode 100644 index 0000000..476b644 --- /dev/null +++ b/java-quarkus/src/main/docker/Dockerfile.jvm @@ -0,0 +1,94 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./gradlew build +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/kontor-quarkus-jvm . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus-jvm +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus-jvm +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 + +ENV LANGUAGE='en_US:en' + + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=185 build/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 build/quarkus-app/*.jar /deployments/ +COPY --chown=185 build/quarkus-app/app/ /deployments/app/ +COPY --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 +ENV AB_JOLOKIA_OFF="" +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + diff --git a/java-quarkus/src/main/docker/Dockerfile.legacy-jar b/java-quarkus/src/main/docker/Dockerfile.legacy-jar new file mode 100644 index 0000000..8f309c6 --- /dev/null +++ b/java-quarkus/src/main/docker/Dockerfile.legacy-jar @@ -0,0 +1,90 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=legacy-jar +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/kontor-quarkus-legacy-jar . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus-legacy-jar +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus-legacy-jar +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 + +ENV LANGUAGE='en_US:en' + + +COPY build/lib/* /deployments/lib/ +COPY build/*-runner.jar /deployments/quarkus-run.jar + +EXPOSE 8080 +USER 185 +ENV AB_JOLOKIA_OFF="" +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" diff --git a/java-quarkus/src/main/docker/Dockerfile.native b/java-quarkus/src/main/docker/Dockerfile.native new file mode 100644 index 0000000..d5a28b2 --- /dev/null +++ b/java-quarkus/src/main/docker/Dockerfile.native @@ -0,0 +1,27 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=native +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t quarkus/kontor-quarkus . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus +# +### +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root build/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/java-quarkus/src/main/docker/Dockerfile.native-micro b/java-quarkus/src/main/docker/Dockerfile.native-micro new file mode 100644 index 0000000..55df6f9 --- /dev/null +++ b/java-quarkus/src/main/docker/Dockerfile.native-micro @@ -0,0 +1,30 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# It uses a micro base image, tuned for Quarkus native executables. +# It reduces the size of the resulting container image. +# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=native +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/kontor-quarkus . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus +# +### +FROM quay.io/quarkus/quarkus-micro-image:1.0 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root build/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/java-quarkus/src/main/java/de/thpeetz/kontor/comics/ComicsResource.java b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/ComicsResource.java new file mode 100644 index 0000000..f6518b9 --- /dev/null +++ b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/ComicsResource.java @@ -0,0 +1,95 @@ +package de.thpeetz.kontor.comics; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import de.thpeetz.kontor.comics.service.ArtistService; +import de.thpeetz.kontor.comics.service.ComicsService; +import de.thpeetz.kontor.comics.service.PublisherService; + +@Path("/kontor/comics") +public class ComicsResource { + + @Inject + transient ComicsService comicsService; + + @Inject + transient ArtistService artistService; + + @Inject + transient PublisherService publisherService; + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/") + public String showComicList() { + return comicsService.showComicList(); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/artist") + public String showArtistList() { + return artistService.showArtistList(); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/artist") + public String showArtistListJson() { + return artistService.showArtistList(); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/artist/view/{artist_id}") + public String showArtist(String artist_id) { + return artistService.showArtist(artist_id); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/artist/create") + public String showArtistCreation() { + return artistService.showArtistCreation(); + } + + @POST + @Produces(MediaType.TEXT_HTML) + @Path("/artist/create") + public String validateArtistDetails() { + return artistService.validateArtistDetails(); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/publisher") + public String showPublisherList() { + return publisherService.showPublisherList(); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/publisher/view/{publisher_id}") + public String showPublisher(String publisher_id) { + return publisherService.showPublisher(publisher_id); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/publisher/create") + public String showPublisherCreation() { + return publisherService.showPublisherCreation(); + } + + @POST + @Produces(MediaType.TEXT_HTML) + @Path("/publisher/create") + public String validatePublisherDetails() { + return publisherService.validatePublisherDetails(); + } +} diff --git a/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ArtistService.java b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ArtistService.java new file mode 100644 index 0000000..cdef9aa --- /dev/null +++ b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ArtistService.java @@ -0,0 +1,28 @@ +package de.thpeetz.kontor.comics.service; + +import javax.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class ArtistService { + + public String showArtistList() { + // TODO Auto-generated method stub + return null; + } + + public String showArtist(String artist_id) { + // TODO Auto-generated method stub + return null; + } + + public String showArtistCreation() { + // TODO Auto-generated method stub + return null; + } + + public String validateArtistDetails() { + // TODO Auto-generated method stub + return null; + } + +} diff --git a/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ComicsService.java b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ComicsService.java new file mode 100644 index 0000000..ee58e17 --- /dev/null +++ b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ComicsService.java @@ -0,0 +1,12 @@ +package de.thpeetz.kontor.comics.service; + +import javax.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class ComicsService { + + public String showComicList() { + return "comics"; + } + +} diff --git a/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/PublisherService.java b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/PublisherService.java new file mode 100644 index 0000000..d3414b1 --- /dev/null +++ b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/PublisherService.java @@ -0,0 +1,28 @@ +package de.thpeetz.kontor.comics.service; + +import javax.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class PublisherService { + + public String showPublisherList() { + // TODO Auto-generated method stub + return null; + } + + public String showPublisher(String publisher_id) { + // TODO Auto-generated method stub + return null; + } + + public String showPublisherCreation() { + // TODO Auto-generated method stub + return null; + } + + public String validatePublisherDetails() { + // TODO Auto-generated method stub + return null; + } + +} diff --git a/java-quarkus/src/main/resources/META-INF/resources/index.html b/java-quarkus/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000..609ae3a --- /dev/null +++ b/java-quarkus/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,284 @@ + + + + + kontor-quarkus - 1.0.0-SNAPSHOT + + + +
+
+
+ + + + + quarkus_logo_horizontal_rgb_1280px_reverse + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+

You just made a Quarkus application.

+

This page is served by Quarkus.

+ Visit the Dev UI +

This page: src/main/resources/META-INF/resources/index.html

+

App configuration: src/main/resources/application.properties

+

Static assets: src/main/resources/META-INF/resources/

+

Code: src/main/java

+

Generated starter code:

+
    +
  • + RESTEasy Reactive Easily start your Reactive RESTful Web Services +
    @Path: /hello +
    Related guide +
  • + +
+
+
+

Selected extensions

+
    +
  • RESTEasy Reactive (guide)
  • +
  • RESTEasy Reactive Jackson
  • +
+
Documentation
+

Practical step-by-step guides to help you achieve a specific goal. Use them to help get your work + done.

+
Set up your IDE
+

Everyone has a favorite IDE they like to use to code. Learn how to configure yours to maximize your + Quarkus productivity.

+
+
+
+ + diff --git a/java-quarkus/src/main/resources/application.properties b/java-quarkus/src/main/resources/application.properties new file mode 100644 index 0000000..21328a1 --- /dev/null +++ b/java-quarkus/src/main/resources/application.properties @@ -0,0 +1,3 @@ +# Your configuration properties +quarkus.http.test-port=8083 +quarkus.http.test-ssl-port=8446 diff --git a/java-quarkus/src/native-test/java/de/thpeetz/kontor/comics/ComicsResourceIT.java b/java-quarkus/src/native-test/java/de/thpeetz/kontor/comics/ComicsResourceIT.java new file mode 100644 index 0000000..230d951 --- /dev/null +++ b/java-quarkus/src/native-test/java/de/thpeetz/kontor/comics/ComicsResourceIT.java @@ -0,0 +1,8 @@ +package de.thpeetz.kontor.comics; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class ComicsResourceIT extends ComicsResourceTest { + +} diff --git a/java-quarkus/src/test/java/de/thpeetz/kontor/comics/ComicsResourceTest.java b/java-quarkus/src/test/java/de/thpeetz/kontor/comics/ComicsResourceTest.java new file mode 100644 index 0000000..25dcd49 --- /dev/null +++ b/java-quarkus/src/test/java/de/thpeetz/kontor/comics/ComicsResourceTest.java @@ -0,0 +1,19 @@ +package de.thpeetz.kontor.comics; + +import org.junit.jupiter.api.Test; +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class ComicsResourceTest { + + @Test + void testComicsEndpoint() { + given().when().get("/kontor/comics") + .then() + .statusCode(200) + .body(is("comics")); + } +} From f1d36acff78d92f24c3ebcbd11ab9e3762b21685 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Fri, 10 Jan 2025 17:39:54 +0100 Subject: [PATCH 38/91] add sqlalchemy as orm tool --- gui/comic_model.py | 89 -------------------------- gui/data.py | 79 ----------------------- gui/database/__init__.py | 134 +++++++++++++++++++++++++++++++++++++++ gui/database/base.py | 7 ++ gui/database/comic.py | 40 ++++++++++++ gui/database/media.py | 25 ++++++++ gui/database/metadata.py | 49 ++++++++++++++ gui/kontor.py | 3 +- gui/model_config.py | 7 +- gui/table_model.py | 20 ++++++ 10 files changed, 281 insertions(+), 172 deletions(-) delete mode 100644 gui/comic_model.py delete mode 100644 gui/data.py create mode 100644 gui/database/__init__.py create mode 100644 gui/database/base.py create mode 100644 gui/database/comic.py create mode 100644 gui/database/media.py create mode 100644 gui/database/metadata.py diff --git a/gui/comic_model.py b/gui/comic_model.py deleted file mode 100644 index 2472670..0000000 --- a/gui/comic_model.py +++ /dev/null @@ -1,89 +0,0 @@ -from datetime import datetime - -import mariadb -from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt -from PySide6.QtGui import QColor - - -class ComicTableModel(QAbstractTableModel): - - def __init__(self, db_config, main_window): - super().__init__() - self.main_window = main_window - self._data = [] - self.status_bar = main_window.statusBar - self.mariadb_conn = mariadb.connect( - host=db_config['mariadb']['host'], - port=db_config['mariadb']['port'], - user=db_config['mariadb']['user'], - password=db_config['mariadb']['password'], - database=db_config['mariadb']['database'] - ) - self.refresh() - - def refresh(self): - data = [] - cursor = self.mariadb_conn.cursor() - cursor.execute("SELECT id, created_date, last_modified_date, title, publisher_id FROM comic") - rows = cursor.fetchall() - for row in rows: - data.append(list(row)) - self.status_bar.showMessage(f"{len(rows)} Einträge geladen", 3000) - self._data = data - - def rowCount(self, parent=QModelIndex()): - # The length of the outer list. - return len(self._data) - - def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): - if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: - match col: - case 0: - return "ID" - case 1: - return "Created" - case 2: - return "Updated" - case 3: - return "Title" - case 4: - return "Verlag" - if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: - return str(col + 1) - - def data(self, index, role=Qt.ItemDataRole.DisplayRole): - value = self._data[index.row()][index.column()] - if role == Qt.ItemDataRole.DisplayRole: - if isinstance(value, datetime): - return value.strftime("%Y-%m-%d %M:%M:%S") - if isinstance(value, str): - return value - if isinstance(value, bytes): - if value == b'\x01': - return "True" - return "False" - return value - if role == Qt.ItemDataRole.DecorationRole: - if isinstance(value, bytes): - # print('{}: {}'.format(value, type(value))) - if value == b'\x01': - return self.main_window.tick - else: - return self.main_window.cross - - - def columnCount(self, index=QModelIndex()): - # The following takes the first sub-list, and returns - # the length (only works if all rows are an equal length) - return len(self._data[0]) - - def setData(self, index, value, role=Qt.ItemDataRole.EditRole): - if role == Qt.ItemDataRole.EditRole: - self._data[index.row()][index.column()] = value - if role == Qt.ItemDataRole.CheckStateRole: - checked = value == Qt.CheckState.Checked - self._data[index.row()][index.column()] = checked - return True - - def flags(self, index): - return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate diff --git a/gui/data.py b/gui/data.py deleted file mode 100644 index bb4436c..0000000 --- a/gui/data.py +++ /dev/null @@ -1,79 +0,0 @@ -import mariadb - - -class KontorDB: - - def __init__(self, db_config): - self.db_conn = mariadb.connect( - host=db_config['mariadb']['host'], - port=db_config['mariadb']['port'], - user=db_config['mariadb']['user'], - password=db_config['mariadb']['password'], - database=db_config['mariadb']['database'] - ) - - def get_table_id(self, table_name): - cursor = self.db_conn.cursor() - cursor.execute("SELECT id, created_date, last_modified_date FROM meta_data_table WHERE table_name=?", (table_name, )) - row = cursor.fetchone() - cursor.close() - return row[0] - - def get_table_names(self) -> list: - tables_names = [] - cursor = self.db_conn.cursor() - cursor.execute("SELECT id, table_name from meta_data_table") - rows = cursor.fetchall() - for (_, table_name) in rows: - tables_names.append(table_name) - cursor.close() - return tables_names - - def get_column_meta_data(self, table_id): - cursor = self.db_conn.cursor() - meta_data = {} - cursor.execute("SELECT column_name, column_order, column_label FROM meta_data_column WHERE table_id=? AND is_shown is true ORDER bY column_order", (table_id, )) - rows = cursor.fetchall() - order = 0 - for (column_name, column_order, column_label) in rows: - meta_data[order] = { 'column': column_name, 'label': column_label, 'order': column_order} - order += 1 - cursor.close() - # print(f"retrieved {len(rows)} columns, set {len(meta_data)} headers") - return meta_data - - def get_filters(self, table_id): - cursor = self.db_conn.cursor() - filters = {} - cursor.execute("SELECT column_name, filter_label from meta_data_column WHERE table_id=? AND show_filter is true", (table_id, )) - rows = cursor.fetchall() - for row in rows: - filters[row[0]] = {'label': row[1], 'widget': None} - cursor.close() - # print(f"retrieved {len(rows)} filters: {filters}") - return filters - - def get_data(self, table_name: str, columns: dict, where_clause: str) -> list: - data = [] - cursor = self.db_conn.cursor() - cursor.execute(self.get_statement(table_name, columns, where_clause)) - rows = cursor.fetchall() - print(len(rows)) - for row in rows: - # print(f"KontorDB.get_data: {row}") - data.append(list(row)) - cursor.close() - print(f"KontorDB.getData: return {len(data)}") - return data - - def get_statement(self, table: str, header: dict, where_clause): - columns = "" - for index, column in header.items(): - if index > 0: - columns += ", " - columns += column['column'] - if len(columns) == 0: - columns = "*" - statement = f"SELECT {columns} FROM {table} {where_clause}" - print(f"{statement=}") - return statement diff --git a/gui/database/__init__.py b/gui/database/__init__.py new file mode 100644 index 0000000..122959e --- /dev/null +++ b/gui/database/__init__.py @@ -0,0 +1,134 @@ +import mariadb +from sqlalchemy import create_engine, text, MetaData, join +from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker + +from database.base import Base +from database.comic import Comic +from database.metadata import MetaDataTable, MetaDataColumn + + +class KontorDB: + + def __init__(self, db_config): + self.db_conn = mariadb.connect( + host=db_config['mariadb']['host'], + port=db_config['mariadb']['port'], + user=db_config['mariadb']['user'], + password=db_config['mariadb']['password'], + database=db_config['mariadb']['database'] + ) + connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}' + .format( + db_config['mariadb']['user'], + db_config['mariadb']['password'], + db_config['mariadb']['host'], + db_config['mariadb']['port'], + db_config['mariadb']['database'])) + # engine = create_engine(connect_string, echo=True) + engine = create_engine(connect_string) + Base.metadata.create_all(bind=engine) + __session__ = sessionmaker(bind=engine) + self.session = __session__() + + def get_table_id(self, table_name): + cursor = self.db_conn.cursor() + cursor.execute("SELECT id, created_date, last_modified_date FROM meta_data_table WHERE table_name=?", + (table_name,)) + row = cursor.fetchone() + cursor.close() + # table_ids = self.session.query(MetaDataTable).filter(MetaDataTable.table_name == table_name).all() + # print(type(table_ids)) + return row[0] + + def get_table_names(self) -> list: + tables_names = [] + cursor = self.db_conn.cursor() + cursor.execute("SELECT id, table_name from meta_data_table") + rows = cursor.fetchall().all + for row in rows: + print(row) + for (_, table_name) in rows: + tables_names.append(table_name) + cursor.close() + tables = self.session.query(MetaDataTable).all() + for table in tables: + print(table) + return tables_names + + def get_column_meta_data(self, table_id: str, table_name: str): + cursor = self.db_conn.cursor() + meta_data = {} + cursor.execute( + "SELECT column_name, column_order, column_label FROM meta_data_column WHERE table_id=? AND is_shown is true ORDER bY column_order", + (table_id,)) + rows = cursor.fetchall() + order = 0 + for (column_name, column_order, column_label) in rows: + meta_data[order] = {'column': column_name, 'label': column_label, 'order': column_order} + order += 1 + cursor.close() + columns = (self.session.query(MetaDataColumn). + join(MetaDataTable, MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name). + filter(MetaDataColumn.is_shown is True). + order_by(MetaDataColumn.column_order).all()) + print(columns) + for column in columns: + print(column) + result = repr(column) + if result is not None: + print(result) + # print(f"retrieved {len(rows)} columns, set {len(meta_data)} headers") + return meta_data + + def get_filters(self, table_id): + cursor = self.db_conn.cursor() + filters = {} + cursor.execute( + "SELECT column_name, filter_label from meta_data_column WHERE table_id=? AND show_filter is true", + (table_id,)) + rows = cursor.fetchall() + for row in rows: + filters[row[0]] = {'label': row[1], 'widget': None} + cursor.close() + # print(f"retrieved {len(rows)} filters: {filters}") + return filters + + def get_data(self, table_name: str, columns: dict, where_clause: str) -> list: + data = [] + cursor = self.db_conn.cursor() + cursor.execute(self.get_statement(table_name, columns, where_clause)) + rows = cursor.fetchall() + print(len(rows)) + for row in rows: + # print(f"KontorDB.get_data: {row}") + data.append(list(row)) + cursor.close() + print(f"KontorDB.getData: return {len(data)}") + if table_name == 'comic' and len(where_clause) == 0: + data.clear() + comics = self.session.query(Comic).all() + for item in comics: + # print(item) + row = [] + for order in columns.keys(): + column_name = columns[order]['column'] + if column_name == 'publisher_id': + row.append(item.publisher.name) + else: + row.append(getattr(item, column_name)) + # print(repr(row)) + data.append(row) + return data + + def get_statement(self, table: str, header: dict, where_clause): + columns = "" + for index, column in header.items(): + if index > 0: + columns += ", " + columns += column['column'] + if len(columns) == 0: + columns = "*" + statement = f"SELECT {columns} FROM {table} {where_clause}" + print(f"{statement=}") + return statement diff --git a/gui/database/base.py b/gui/database/base.py new file mode 100644 index 0000000..fb74a49 --- /dev/null +++ b/gui/database/base.py @@ -0,0 +1,7 @@ +from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker + + +class Base(DeclarativeBase): + pass + + diff --git a/gui/database/comic.py b/gui/database/comic.py new file mode 100644 index 0000000..c17244e --- /dev/null +++ b/gui/database/comic.py @@ -0,0 +1,40 @@ +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from database.base import Base + + +class Publisher(Base): + __tablename__ = "publisher" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255)) + comics = relationship("Comic") + + def __repr__(self): + return f'Publisher({self.id} {self.name})' + + def __str__(self): + return self.__repr__() + + +class Comic(Base): + __tablename__ = 'comic' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + title = Column(String(length=255)) + publisher_id = Column(String, ForeignKey('publisher.id')) + publisher = relationship("Publisher", back_populates="comics") + current_order = Column(BIT(1)) + completed = Column(BIT(1)) + + def __repr__(self): + return f'Comic({self.id} {self.version} {self.title} {self.publisher.name})' + + def __str__(self): + return f'{self.title}({self.id})' diff --git a/gui/database/media.py b/gui/database/media.py new file mode 100644 index 0000000..ebebc57 --- /dev/null +++ b/gui/database/media.py @@ -0,0 +1,25 @@ +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from database.base import Base + + +class MediaFile(Base): + __tablename__ = 'media_file' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + cloud_link = Column(String(255)) + file_name = Column(String(255)) + path = Column(String(255)) + review = Column(Boolean, default=True) + title = Column(String(255)) + url = Column(String(255)) + should_download = Column(Boolean, default=True) + + def __repr__(self): + return f'MediaFile({self.id} {self.title} {self.title})' + + def __str__(self): + return f'{self.title}({self.id})' diff --git a/gui/database/metadata.py b/gui/database/metadata.py new file mode 100644 index 0000000..8a59633 --- /dev/null +++ b/gui/database/metadata.py @@ -0,0 +1,49 @@ +from sqlalchemy import Column, String, ForeignKey, DateTime, Integer, Boolean +from sqlalchemy.orm import relationship + +from database import Base + + +class MetaDataTable(Base): + __tablename__ = 'meta_data_table' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + table_name = Column(String(255), unique=True) + table_columns = relationship("MetaDataColumn") + + def __repr__(self): + print(f'MetaDataTable({self.id} {self.table_name})') + + def __str__(self): + print(f'{self.table_name}({self.id})') + + +class MetaDataColumn(Base): + __tablename__ = 'meta_data_column' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + column_modifier = Column(String(255), nullable=True) + column_name = Column(String(255)) + column_order = Column(Integer) + column_sync_name = Column(String(255)) + column_type = Column(String(255)) + table_id = Column(String, ForeignKey('meta_data_table.id')) + table = relationship("MetaDataTable", back_populates="table_columns") + column_label = Column(String(255)) + filter_label = Column(String(255)) + is_shown = Column(Boolean) + show_filter = Column(Boolean) + + def __repr__(self): + if self.column_name is None: + print(f'MetaDataColumn({self.id} {self.table.table_name}.__)') + else: + print(f'MetaDataColumn({self.id} {self.table.table_name}.{self.column_name})') + + def __str__(self): + print(f'{self.column_name}({self.id})') + diff --git a/gui/kontor.py b/gui/kontor.py index 59e770c..ea53ba5 100644 --- a/gui/kontor.py +++ b/gui/kontor.py @@ -10,9 +10,8 @@ from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidg from PySide6.QtWidgets import QApplication, QLabel, QMainWindow from platformdirs import PlatformDirs -from comic_model import ComicTableModel +from database import KontorDB from dialogs import ExportKontorDialog, ImportKontorDialog -from data import KontorDB from model_config import KontorModelConfig from table_model import KontorTableModel from media_file_model import MediaFileTableModel diff --git a/gui/model_config.py b/gui/model_config.py index ba3e510..abff7da 100644 --- a/gui/model_config.py +++ b/gui/model_config.py @@ -1,7 +1,8 @@ import mariadb from PySide6.QtWidgets import QHBoxLayout, QCheckBox -from data import KontorDB +from database import KontorDB +from database.comic import Comic class KontorModelConfig: @@ -23,7 +24,7 @@ class KontorModelConfig: def get_table_config(self): if self._table_id is None: self.get_table_id() - self.header = self.kontor_db.get_column_meta_data(self._table_id) + self.header = self.kontor_db.get_column_meta_data(self._table_id, self._table) self.filter = self.kontor_db.get_filters(self._table_id) def get_filter(self) -> str: @@ -44,6 +45,8 @@ class KontorModelConfig: def get_data(self) -> list: data = self.kontor_db.get_data(self._table, self.header, self.get_filter()) print(f"KontorModelConfig.get_data: {len(data)}") + # comics = self.kontor_db.session.query(Comic).all() + # print(f'{len(comics)} Comics loaded') return data def get_filter_layout(self) -> QHBoxLayout: diff --git a/gui/table_model.py b/gui/table_model.py index a28e1cc..2af11c9 100644 --- a/gui/table_model.py +++ b/gui/table_model.py @@ -56,6 +56,16 @@ class KontorTableModel(QAbstractTableModel): return self._main_window.tick else: return self._main_window.cross + if isinstance(value, int): + if value == 1: + return self._main_window.tick + else: + return self._main_window.cross + if isinstance(value, bool): + if value: + return self._main_window.tick + else: + return self._main_window.cross return str(value) if role == Qt.ItemDataRole.DecorationRole: if isinstance(value, bytes): @@ -64,6 +74,16 @@ class KontorTableModel(QAbstractTableModel): return self._main_window.tick else: return self._main_window.cross + if isinstance(value, int): + if value == 1: + return self._main_window.tick + else: + return self._main_window.cross + if isinstance(value, bool): + if value: + return self._main_window.tick + else: + return self._main_window.cross def columnCount(self, index=QModelIndex()): # The following takes the first sub-list, and returns From d2b1cb999adcb7366588c54141c735545703b3d7 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sat, 11 Jan 2025 01:54:05 +0100 Subject: [PATCH 39/91] reorganize files for Qt application --- gui/resources.py | 7 -- qt/.gitignore | 2 + {gui => qt}/database/__init__.py | 50 ++---------- {gui => qt}/database/base.py | 0 {gui => qt}/database/comic.py | 0 {gui => qt}/database/media.py | 0 {gui => qt}/database/metadata.py | 11 ++- qt/gui/__init__.py | 0 {gui => qt/gui}/dialogs.py | 0 gui/kontor.py => qt/gui/main_window.py | 21 +---- {gui => qt/gui}/model_config.py | 1 - {gui => qt/gui}/table_model.py | 2 +- qt/kontor.py | 21 +++++ qt/pysidedeploy.spec | 98 ++++++++++++++++++++++++ {gui => qt}/res/application-export.png | Bin {gui => qt}/res/application-import.png | Bin {gui => qt}/res/arrow-circle-double.png | Bin {gui => qt}/res/cross.png | Bin {gui => qt}/res/tick.png | Bin 19 files changed, 136 insertions(+), 77 deletions(-) delete mode 100644 gui/resources.py create mode 100644 qt/.gitignore rename {gui => qt}/database/__init__.py (63%) rename {gui => qt}/database/base.py (100%) rename {gui => qt}/database/comic.py (100%) rename {gui => qt}/database/media.py (100%) rename {gui => qt}/database/metadata.py (80%) create mode 100644 qt/gui/__init__.py rename {gui => qt/gui}/dialogs.py (100%) rename gui/kontor.py => qt/gui/main_window.py (90%) rename {gui => qt/gui}/model_config.py (98%) rename {gui => qt/gui}/table_model.py (98%) create mode 100644 qt/kontor.py create mode 100755 qt/pysidedeploy.spec rename {gui => qt}/res/application-export.png (100%) rename {gui => qt}/res/application-import.png (100%) rename {gui => qt}/res/arrow-circle-double.png (100%) rename {gui => qt}/res/cross.png (100%) rename {gui => qt}/res/tick.png (100%) diff --git a/gui/resources.py b/gui/resources.py deleted file mode 100644 index c135d07..0000000 --- a/gui/resources.py +++ /dev/null @@ -1,7 +0,0 @@ -from PySide6.QtGui import QIcon - -tick = QIcon('tick.png') -cross = QIcon('cross.png') -import_icon = QIcon("application-import.png") -export_icon = QIcon("application-export.png") -circle_icon = QIcon("arrow-circle-double.png") diff --git a/qt/.gitignore b/qt/.gitignore new file mode 100644 index 0000000..38b154f --- /dev/null +++ b/qt/.gitignore @@ -0,0 +1,2 @@ +deployment/ +kontor.bin diff --git a/gui/database/__init__.py b/qt/database/__init__.py similarity index 63% rename from gui/database/__init__.py rename to qt/database/__init__.py index 122959e..c6d21a1 100644 --- a/gui/database/__init__.py +++ b/qt/database/__init__.py @@ -1,5 +1,5 @@ import mariadb -from sqlalchemy import create_engine, text, MetaData, join +from sqlalchemy import create_engine, select, text, MetaData, join from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker from database.base import Base @@ -31,54 +31,20 @@ class KontorDB: self.session = __session__() def get_table_id(self, table_name): - cursor = self.db_conn.cursor() - cursor.execute("SELECT id, created_date, last_modified_date FROM meta_data_table WHERE table_name=?", - (table_name,)) - row = cursor.fetchone() - cursor.close() - # table_ids = self.session.query(MetaDataTable).filter(MetaDataTable.table_name == table_name).all() - # print(type(table_ids)) - return row[0] + result = self.session.execute(select(MetaDataTable.id).where(MetaDataTable.table_name == table_name)).scalar() + return result def get_table_names(self) -> list: - tables_names = [] - cursor = self.db_conn.cursor() - cursor.execute("SELECT id, table_name from meta_data_table") - rows = cursor.fetchall().all - for row in rows: - print(row) - for (_, table_name) in rows: - tables_names.append(table_name) - cursor.close() tables = self.session.query(MetaDataTable).all() - for table in tables: - print(table) - return tables_names + result = [table.table_name for table in tables] + return result - def get_column_meta_data(self, table_id: str, table_name: str): - cursor = self.db_conn.cursor() + def get_column_meta_data(self, table_id: str, table_name: str) -> dict: meta_data = {} - cursor.execute( - "SELECT column_name, column_order, column_label FROM meta_data_column WHERE table_id=? AND is_shown is true ORDER bY column_order", - (table_id,)) - rows = cursor.fetchall() order = 0 - for (column_name, column_order, column_label) in rows: - meta_data[order] = {'column': column_name, 'label': column_label, 'order': column_order} + for (_, column) in self.session.query(MetaDataTable, MetaDataColumn).filter(MetaDataTable.id == MetaDataColumn.table_id).filter(MetaDataTable.table_name == table_name).filter(MetaDataColumn.is_shown == 1).all(): + meta_data[order] = {'column': column.column_name, 'label': column.column_label, 'order': column.column_order} order += 1 - cursor.close() - columns = (self.session.query(MetaDataColumn). - join(MetaDataTable, MetaDataTable.id == MetaDataColumn.table_id). - filter(MetaDataTable.table_name == table_name). - filter(MetaDataColumn.is_shown is True). - order_by(MetaDataColumn.column_order).all()) - print(columns) - for column in columns: - print(column) - result = repr(column) - if result is not None: - print(result) - # print(f"retrieved {len(rows)} columns, set {len(meta_data)} headers") return meta_data def get_filters(self, table_id): diff --git a/gui/database/base.py b/qt/database/base.py similarity index 100% rename from gui/database/base.py rename to qt/database/base.py diff --git a/gui/database/comic.py b/qt/database/comic.py similarity index 100% rename from gui/database/comic.py rename to qt/database/comic.py diff --git a/gui/database/media.py b/qt/database/media.py similarity index 100% rename from gui/database/media.py rename to qt/database/media.py diff --git a/gui/database/metadata.py b/qt/database/metadata.py similarity index 80% rename from gui/database/metadata.py rename to qt/database/metadata.py index 8a59633..2fd0af7 100644 --- a/gui/database/metadata.py +++ b/qt/database/metadata.py @@ -14,11 +14,10 @@ class MetaDataTable(Base): table_columns = relationship("MetaDataColumn") def __repr__(self): - print(f'MetaDataTable({self.id} {self.table_name})') + return f'MetaDataTable({self.id} {self.table_name})' def __str__(self): - print(f'{self.table_name}({self.id})') - + return f'{self.table_name}({self.id})' class MetaDataColumn(Base): __tablename__ = 'meta_data_column' @@ -40,10 +39,10 @@ class MetaDataColumn(Base): def __repr__(self): if self.column_name is None: - print(f'MetaDataColumn({self.id} {self.table.table_name}.__)') + return f'MetaDataColumn({self.id} {self.table.table_name}.__)' else: - print(f'MetaDataColumn({self.id} {self.table.table_name}.{self.column_name})') + return f'MetaDataColumn({self.id} {self.table.table_name}.{self.column_name})' def __str__(self): - print(f'{self.column_name}({self.id})') + return f'{self.column_name}({self.id})' diff --git a/qt/gui/__init__.py b/qt/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gui/dialogs.py b/qt/gui/dialogs.py similarity index 100% rename from gui/dialogs.py rename to qt/gui/dialogs.py diff --git a/gui/kontor.py b/qt/gui/main_window.py similarity index 90% rename from gui/kontor.py rename to qt/gui/main_window.py index ea53ba5..ea288df 100644 --- a/gui/kontor.py +++ b/qt/gui/main_window.py @@ -1,14 +1,6 @@ -""" -PyQT6 GUI for Kontor -""" -import sys -from pathlib import Path - -import yaml from PySide6.QtGui import QAction, QIcon from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView -from PySide6.QtWidgets import QApplication, QLabel, QMainWindow -from platformdirs import PlatformDirs +from PySide6.QtWidgets import QLabel, QMainWindow from database import KontorDB from dialogs import ExportKontorDialog, ImportKontorDialog @@ -138,14 +130,3 @@ class MainWindow(QMainWindow): layout.addWidget(table_view) model.refresh() return data_tab - - -if __name__ == '__main__': - app = QApplication(sys.argv) - dirs = PlatformDirs("kontor") - database_config = Path(dirs.user_config_dir, 'database-config.yaml') - with open(database_config, 'rt') as f: - db_config = yaml.safe_load(f.read()) - window = MainWindow(db_config) - window.show() - app.exec() diff --git a/gui/model_config.py b/qt/gui/model_config.py similarity index 98% rename from gui/model_config.py rename to qt/gui/model_config.py index abff7da..01d3805 100644 --- a/gui/model_config.py +++ b/qt/gui/model_config.py @@ -2,7 +2,6 @@ import mariadb from PySide6.QtWidgets import QHBoxLayout, QCheckBox from database import KontorDB -from database.comic import Comic class KontorModelConfig: diff --git a/gui/table_model.py b/qt/gui/table_model.py similarity index 98% rename from gui/table_model.py rename to qt/gui/table_model.py index 2af11c9..5e80c3b 100644 --- a/gui/table_model.py +++ b/qt/gui/table_model.py @@ -3,7 +3,7 @@ from datetime import datetime from PySide6.QtCore import QAbstractTableModel, QModelIndex from PySide6.QtGui import Qt -from model_config import KontorModelConfig +from gui.model_config import KontorModelConfig class KontorTableModel(QAbstractTableModel): diff --git a/qt/kontor.py b/qt/kontor.py new file mode 100644 index 0000000..2eed73b --- /dev/null +++ b/qt/kontor.py @@ -0,0 +1,21 @@ +""" +PyQT6 GUI for Kontor +""" +import sys +from pathlib import Path +from platformdirs import PlatformDirs +from PySide6.QtWidgets import QApplication +import yaml + +from gui.main_window import MainWindow + + +if __name__ == '__main__': + app = QApplication(sys.argv) + dirs = PlatformDirs("kontor") + database_config = Path(dirs.user_config_dir, 'database-config.yaml') + with open(database_config, 'rt') as f: + db_config = yaml.safe_load(f.read()) + window = MainWindow(db_config) + window.show() + app.exec() diff --git a/qt/pysidedeploy.spec b/qt/pysidedeploy.spec new file mode 100755 index 0000000..8e65745 --- /dev/null +++ b/qt/pysidedeploy.spec @@ -0,0 +1,98 @@ +[app] + +# title of your application +title = kontor + +# project directory. the general assumption is that project_dir is the parent directory +# of input_file +project_dir = /home/tpeetz/projects/kontor/qt + +# source file path +input_file = /home/tpeetz/projects/kontor/qt/kontor.py + +# directory where exec is stored +exec_directory = . + +# path to .pyproject project file +project_file = + +# application icon +icon = /usr/local/lib/python3.11/dist-packages/PySide6/scripts/deploy_lib/pyside_icon.jpg + +[python] + +# python path +python_path = /usr/bin/python3 + +# python packages to install +packages = Nuitka==2.4.8 + +# buildozer = for deploying Android application +android_packages = buildozer==1.5.0,cython==0.29.33 + +[qt] + +# comma separated path to qml files required +# normally all the qml files required by the project are added automatically +qml_files = + +# excluded qml plugin binaries +excluded_qml_plugins = + +# qt modules used. comma separated +modules = Gui,DBus,Core,Widgets + +# qt plugins used by the application +plugins = platformthemes,imageformats,platforms,generic,iconengines,egldeviceintegrations,styles,xcbglintegrations,platforms/darwin,platforminputcontexts,accessiblebridge + +[android] + +# path to pyside wheel +wheel_pyside = + +# path to shiboken wheel +wheel_shiboken = + +# plugins to be copied to libs folder of the packaged application. comma separated +plugins = + +[nuitka] + +# usage description for permissions requested by the app as found in the info.plist file +# of the app bundle +# eg = extra_args = --show-modules --follow-stdlib +macos.permissions = + +# mode of using nuitka. accepts standalone or onefile. default is onefile. +mode = onefile + +# (str) specify any extra nuitka arguments +extra_args = --quiet --noinclude-qt-translations + +[buildozer] + +# build mode +# possible options = [release, debug] +# release creates an aab, while debug creates an apk +mode = debug + +# contrains path to pyside6 and shiboken6 recipe dir +recipe_dir = + +# path to extra qt android jars to be loaded by the application +jars_dir = + +# if empty uses default ndk path downloaded by buildozer +ndk_path = + +# if empty uses default sdk path downloaded by buildozer +sdk_path = + +# other libraries to be loaded. comma separated. +# loaded at app startup +local_libs = + +# architecture of deployed platform +# possible values = ["aarch64", "armv7a", "i686", "x86_64"] +arch = + diff --git a/gui/res/application-export.png b/qt/res/application-export.png similarity index 100% rename from gui/res/application-export.png rename to qt/res/application-export.png diff --git a/gui/res/application-import.png b/qt/res/application-import.png similarity index 100% rename from gui/res/application-import.png rename to qt/res/application-import.png diff --git a/gui/res/arrow-circle-double.png b/qt/res/arrow-circle-double.png similarity index 100% rename from gui/res/arrow-circle-double.png rename to qt/res/arrow-circle-double.png diff --git a/gui/res/cross.png b/qt/res/cross.png similarity index 100% rename from gui/res/cross.png rename to qt/res/cross.png diff --git a/gui/res/tick.png b/qt/res/tick.png similarity index 100% rename from gui/res/tick.png rename to qt/res/tick.png From 5affa16505b17517ab636fcc565417a655f64e29 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sat, 11 Jan 2025 19:00:58 +0100 Subject: [PATCH 40/91] add tysc schema --- qt/database/base.py | 2 +- qt/database/media.py | 6 +- qt/database/metadata.py | 6 +- qt/database/tysc.py | 68 +++++++++++++++++++ .../de/thpeetz/kontor/tysc/data/Team.java | 1 - 5 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 qt/database/tysc.py diff --git a/qt/database/base.py b/qt/database/base.py index fb74a49..9339e51 100644 --- a/qt/database/base.py +++ b/qt/database/base.py @@ -1,7 +1,7 @@ +from sqlalchemy import Column, String, DateTime, Integer from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker class Base(DeclarativeBase): pass - diff --git a/qt/database/media.py b/qt/database/media.py index ebebc57..621fdce 100644 --- a/qt/database/media.py +++ b/qt/database/media.py @@ -1,5 +1,5 @@ -from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String -from sqlalchemy.orm import relationship +from sqlalchemy import Boolean, Column, DateTime, Integer, String +from sqlalchemy.dialects.mysql import BIT from database.base import Base @@ -13,7 +13,7 @@ class MediaFile(Base): cloud_link = Column(String(255)) file_name = Column(String(255)) path = Column(String(255)) - review = Column(Boolean, default=True) + review = Column(BIT(1), default=True) title = Column(String(255)) url = Column(String(255)) should_download = Column(Boolean, default=True) diff --git a/qt/database/metadata.py b/qt/database/metadata.py index 2fd0af7..e1b9084 100644 --- a/qt/database/metadata.py +++ b/qt/database/metadata.py @@ -1,4 +1,5 @@ from sqlalchemy import Column, String, ForeignKey, DateTime, Integer, Boolean +from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import relationship from database import Base @@ -34,8 +35,8 @@ class MetaDataColumn(Base): table = relationship("MetaDataTable", back_populates="table_columns") column_label = Column(String(255)) filter_label = Column(String(255)) - is_shown = Column(Boolean) - show_filter = Column(Boolean) + is_shown = Column(BIT(1)) + show_filter = Column(BIT(1)) def __repr__(self): if self.column_name is None: @@ -45,4 +46,3 @@ class MetaDataColumn(Base): def __str__(self): return f'{self.column_name}({self.id})' - diff --git a/qt/database/tysc.py b/qt/database/tysc.py new file mode 100644 index 0000000..e78ff17 --- /dev/null +++ b/qt/database/tysc.py @@ -0,0 +1,68 @@ +from sqlalchemy import Boolean, Column, DateTime, Integer, String, ForeignKey +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from database.base import Base + + +class Sport(Base): + __tablename__ = "sport" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255)) + teams = relationship("Team") + positions = relationship("FieldPosition") + + +class Team(Base): + __tablename__ = "team" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255)) + short_name = Column(String(255)) + sport_id = Column(String, ForeignKey("sport.id")) + sport = relationship("Sport", back_populates="positions") + roosters = relationship("Rooster") + + +class FieldPosition(Base): + __tablename__ = "field_position" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255)) + short_name = Column(String(255)) + sport_id = Column(String, ForeignKey("sport.id")) + sport = relationship("Sport", back_populates="positions") + roosters = relationship("Rooster") + + +class Player(Base): + __tablename__ = "player" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + first_name = Column(String(255)) + last_name = Column(String(255)) + roosters = relationship("Rooster") + + +class Rooster(Base): + __tablename__ = "rooster" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + year = Column(Integer) + team_id = Column(String, ForeignKey("team.id")) + team = relationship("Team", back_populates="roosters") + player_id = Column(String, ForeignKey("player.id")) + player = relationship("Player", back_populates="roosters") + position_id = Column(String, ForeignKey("field_position.id")) + position = relationship("roosters") diff --git a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Team.java b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Team.java index b61fdd2..353da78 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Team.java +++ b/springboot/src/main/java/de/thpeetz/kontor/tysc/data/Team.java @@ -1,7 +1,6 @@ package de.thpeetz.kontor.tysc.data; import java.util.List; -import java.util.Objects; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; From 954dab289ad74ceb3360fa9e671565a9e5f38575 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 12 Jan 2025 02:41:40 +0100 Subject: [PATCH 41/91] extend MetaDataColumn by adding column for name of column for referenced tables --- qt/database/__init__.py | 13 ++- qt/database/comic.py | 96 ++++++++++++++++++- qt/database/metadata.py | 1 + qt/database/tysc.py | 89 ++++++++++++++--- qt/gui/main_window.py | 4 + qt/gui/model_config.py | 2 +- .../kontor/admin/SetupModuleAdmin.java | 6 +- .../kontor/admin/data/MetaDataColumn.java | 2 + .../admin/services/MetaDataService.java | 9 ++ .../kontor/admin/views/MetaDataForm.java | 2 + .../kontor/admin/views/MetaDataView.java | 4 +- 11 files changed, 203 insertions(+), 25 deletions(-) diff --git a/qt/database/__init__.py b/qt/database/__init__.py index c6d21a1..c0191cc 100644 --- a/qt/database/__init__.py +++ b/qt/database/__init__.py @@ -43,7 +43,7 @@ class KontorDB: meta_data = {} order = 0 for (_, column) in self.session.query(MetaDataTable, MetaDataColumn).filter(MetaDataTable.id == MetaDataColumn.table_id).filter(MetaDataTable.table_name == table_name).filter(MetaDataColumn.is_shown == 1).all(): - meta_data[order] = {'column': column.column_name, 'label': column.column_label, 'order': column.column_order} + meta_data[order] = {'column': column.column_name, 'label': column.column_label, 'order': column.column_order, 'ref_column': column.ref_column} order += 1 return meta_data @@ -70,7 +70,7 @@ class KontorDB: # print(f"KontorDB.get_data: {row}") data.append(list(row)) cursor.close() - print(f"KontorDB.getData: return {len(data)}") + # print(f"KontorDB.getData: return {len(data)}") if table_name == 'comic' and len(where_clause) == 0: data.clear() comics = self.session.query(Comic).all() @@ -79,8 +79,13 @@ class KontorDB: row = [] for order in columns.keys(): column_name = columns[order]['column'] - if column_name == 'publisher_id': - row.append(item.publisher.name) + if str(column_name).endswith("_id"): + ref_table = column_name[:-3] + # print(f"{ref_table=}") + ref = getattr(item, ref_table) + value = getattr(ref, "name") + # print(f"{value=}") + row.append(value) else: row.append(getattr(item, column_name)) # print(repr(row)) diff --git a/qt/database/comic.py b/qt/database/comic.py index c17244e..dc4204f 100644 --- a/qt/database/comic.py +++ b/qt/database/comic.py @@ -11,7 +11,7 @@ class Publisher(Base): created_date = Column(DateTime) last_modified_date = Column(DateTime) version = Column(Integer) - name = Column(String(length=255)) + name = Column(String(length=255), unique=True) comics = relationship("Comic") def __repr__(self): @@ -27,14 +27,104 @@ class Comic(Base): created_date = Column(DateTime) last_modified_date = Column(DateTime) version = Column(Integer) - title = Column(String(length=255)) - publisher_id = Column(String, ForeignKey('publisher.id')) + title = Column(String(length=255), unique=True) + publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False) publisher = relationship("Publisher", back_populates="comics") current_order = Column(BIT(1)) completed = Column(BIT(1)) + issues = relationship("Issue") + story_arcs = relationship("StoryArc") + trade_paperbacks = relationship("TradePaperback") + volumes = relationship("Volume") + comic_works = relationship("ComicWork") def __repr__(self): return f'Comic({self.id} {self.version} {self.title} {self.publisher.name})' def __str__(self): return f'{self.title}({self.id})' + + +class Volume(Base): + __tablename__ = "volume" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="volumes") + issues = relationship("Issue") + + +class TradePaperback(Base): + __tablename__ = "trade_paperback" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False) + issue_start = Column(Integer) + issue_end = Column(Integer) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="trade_paperbacks") + + +class StoryArc(Base): + __tablename__ = "story_arc" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="story_arcs") + + +class Issue(Base): + __tablename__ = "issue" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + issue_number = Column(String(255)) + in_stock = Column(BIT(1)) + is_read = Column(BIT(1)) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="issues") + volume_id = Column(String, ForeignKey("volume.id"), nullable=True) + volume = relationship("Volume", back_populates="issues") + + +class Artist(Base): + __tablename__ = "artist" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False) + comic_works = relationship("ComicWork") + + +class Worktype(Base): + __tablename__ = "worktype" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False, unique=True) + comic_works = relationship("ComicWork") + + +class ComicWork(Base): + __tablename__ = "comic_work" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="comic_works") + artist_id = Column(String, ForeignKey("artist.id"), nullable=False) + artist = relationship("Artist", back_populates="comic_works") + worktype_id = Column(String, ForeignKey("worktype.id"), nullable=False) + worktype = relationship("Worktype", back_populates="comic_works") diff --git a/qt/database/metadata.py b/qt/database/metadata.py index e1b9084..46a6274 100644 --- a/qt/database/metadata.py +++ b/qt/database/metadata.py @@ -37,6 +37,7 @@ class MetaDataColumn(Base): filter_label = Column(String(255)) is_shown = Column(BIT(1)) show_filter = Column(BIT(1)) + ref_column = Column(String, nullable=True) def __repr__(self): if self.column_name is None: diff --git a/qt/database/tysc.py b/qt/database/tysc.py index e78ff17..9c94177 100644 --- a/qt/database/tysc.py +++ b/qt/database/tysc.py @@ -1,4 +1,4 @@ -from sqlalchemy import Boolean, Column, DateTime, Integer, String, ForeignKey +from sqlalchemy import Boolean, Column, DateTime, Integer, String, ForeignKey, UniqueConstraint from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import relationship @@ -7,11 +7,14 @@ from database.base import Base class Sport(Base): __tablename__ = "sport" + __table_args__ = ( + UniqueConstraint("name"), + ) id = Column(String, primary_key=True) created_date = Column(DateTime) last_modified_date = Column(DateTime) version = Column(Integer) - name = Column(String(255)) + name = Column(String(255), nullable=False, index=True, unique=True) teams = relationship("Team") positions = relationship("FieldPosition") @@ -22,47 +25,107 @@ class Team(Base): created_date = Column(DateTime) last_modified_date = Column(DateTime) version = Column(Integer) - name = Column(String(255)) - short_name = Column(String(255)) - sport_id = Column(String, ForeignKey("sport.id")) + name = Column(String(255), nullable=False, index=True, unique=True) + short_name = Column(String(255), nullable=False, ) + sport_id = Column(String, ForeignKey("sport.id"), nullable=False) sport = relationship("Sport", back_populates="positions") roosters = relationship("Rooster") class FieldPosition(Base): __tablename__ = "field_position" + __table_args__ = ( + UniqueConstraint("name", "sport_id"), + UniqueConstraint("short_name", "sport_id"), + ) id = Column(String, primary_key=True) created_date = Column(DateTime) last_modified_date = Column(DateTime) version = Column(Integer) - name = Column(String(255)) - short_name = Column(String(255)) - sport_id = Column(String, ForeignKey("sport.id")) + name = Column(String(255), nullable=False, index=True) + short_name = Column(String(255), nullable=False) + sport_id = Column(String, ForeignKey("sport.id"), nullable=False, index=True) sport = relationship("Sport", back_populates="positions") roosters = relationship("Rooster") class Player(Base): __tablename__ = "player" + __table_args__ = ( + UniqueConstraint("first_name", "last_name"), + ) id = Column(String, primary_key=True) created_date = Column(DateTime) last_modified_date = Column(DateTime) version = Column(Integer) - first_name = Column(String(255)) - last_name = Column(String(255)) + first_name = Column(String(255), nullable=False, index=True) + last_name = Column(String(255), nullable=False, index=True) roosters = relationship("Rooster") + def get_full_name(self) -> str: + return f"{self.last_name}, {self.first_name}" + class Rooster(Base): __tablename__ = "rooster" + __table_args__ = ( + UniqueConstraint("year", "team_id", "player_id", "position_id"), + ) id = Column(String, primary_key=True) created_date = Column(DateTime) last_modified_date = Column(DateTime) version = Column(Integer) year = Column(Integer) - team_id = Column(String, ForeignKey("team.id")) + team_id = Column(String, ForeignKey("team.id"), nullable=False, index=True) team = relationship("Team", back_populates="roosters") - player_id = Column(String, ForeignKey("player.id")) + player_id = Column(String, ForeignKey("player.id"), nullable=False, index=True) player = relationship("Player", back_populates="roosters") - position_id = Column(String, ForeignKey("field_position.id")) + position_id = Column(String, ForeignKey("field_position.id"), nullable=False, index=True) position = relationship("roosters") + cards = relationship("Card") + +class Vendor(Base): + __tablename__ = "vendor" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255), nullable=False, unique=True, index=True) + card_sets = relationship("CardSet") + cards = relationship("Card") + + +class CardSet(Base): + __tablename__ = "card_set" + __table_args__ = ( + UniqueConstraint("name", "vendor_id"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255), index=True) + parallel_set = Column(BIT(1)) + insert_set = Column(BIT(1)) + vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True) + vendor = relationship("Vendor", back_populates="card_sets") + cards = relationship("Card") + + +class Card(Base): + __tablename__ = "card" + __table_args__ = ( + UniqueConstraint("card_number", "year", "vendor_id", "card_set_id"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + card_number = Column(Integer, index=True) + year = Column(Integer, index=True) + card_set_id = Column(String, ForeignKey("card_set.id"), nullable=False) + card_set = relationship("cards") + rooster_id = Column(String, ForeignKey("rooster.id"), nullable=False) + rooster = relationship("cards") + vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False) + vendor = relationship("Vendor", back_populates="cards") diff --git a/qt/gui/main_window.py b/qt/gui/main_window.py index ea288df..020326f 100644 --- a/qt/gui/main_window.py +++ b/qt/gui/main_window.py @@ -69,9 +69,13 @@ class MainWindow(QMainWindow): menu_bar.addMenu(kontor_menu) kontor_menu.addAction(self.importAction) kontor_menu.addAction(self.exportAction) + comic_menu = QMenu("&Comic") + tysc_menu = QMenu("&TradeYourSportCards") media_file_menu = QMenu("&MediaFile") media_file_menu.addAction(self.updateTitleAction) media_file_menu.addAction(self.downloadAction) + kontor_menu.addMenu(comic_menu) + kontor_menu.addMenu(tysc_menu) kontor_menu.addMenu(media_file_menu) # Help menu help_menu = QMenu("&Hilfe") diff --git a/qt/gui/model_config.py b/qt/gui/model_config.py index 01d3805..c2644e9 100644 --- a/qt/gui/model_config.py +++ b/qt/gui/model_config.py @@ -43,7 +43,7 @@ class KontorModelConfig: def get_data(self) -> list: data = self.kontor_db.get_data(self._table, self.header, self.get_filter()) - print(f"KontorModelConfig.get_data: {len(data)}") + # print(f"KontorModelConfig.get_data: {len(data)}") # comics = self.kontor_db.session.query(Comic).all() # print(f'{len(comics)} Comics loaded') return data diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java index 4ac89e0..b54f74e 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java @@ -179,7 +179,7 @@ public class SetupModuleAdmin implements ApplicationListener column.getColumnName().equals(columnName))) { log.debug("Column {} with name {} of table {} found, check Values", columnOrder, columnName, table.getTableName()); MetaDataColumn column = table.getTableColumns().get(columnOrder.intValue()-1); @@ -70,6 +74,10 @@ public class MetaDataService { log.debug("filterLabel has to be change to {}}", filterLabel); column.setFilterLabel(filterLabel); } + if (refColumn != null && !refColumn.equals(column.getRefColumn())) { + log.debug("refColumn has to be change to {}}", filterLabel); + column.setRefColumn(refColumn); + } metaDataColumnRepository.save(column); } else { log.info("Column {} of table {} not found, will create it", columnName, table.getTableName()); @@ -85,6 +93,7 @@ public class MetaDataService { } } + public List findAllMetaDataColumns(String stringFilter) { if (stringFilter == null || stringFilter.isEmpty()) { log.debug("Found " + metaDataColumnRepository.count()+ " entries"); diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java index b65b099..481f237 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java @@ -29,6 +29,7 @@ public class MetaDataForm extends FormLayout { TextField columnLabel = new TextField("Column Label"); Checkbox showFilter = new Checkbox("Show Filter"); TextField filterLabel = new TextField("Filter Label"); + TextField refColumn = new TextField("Ref Column"); Button save = new com.vaadin.flow.component.button.Button("Save"); Button delete = new com.vaadin.flow.component.button.Button("Delete"); @@ -47,6 +48,7 @@ public class MetaDataForm extends FormLayout { isShown.addClickListener(click -> columnLabel.setEnabled(isShown.getValue())); add(showFilter, filterLabel); showFilter.addClickListener(click -> filterLabel.setEnabled(showFilter.getValue())); + add(refColumn); add(createButtonsLayout()); } diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java index 780126a..a5f4cca 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java @@ -66,6 +66,8 @@ public class MetaDataView extends VerticalLayout { setHeader("Zeige Filter").setWidth("6rem").setSortable(true); Grid.Column filterLabelColumn = grid.addColumn(MetaDataColumn::getFilterLabel) .setHeader("Filter Name").setResizable(true).setSortable(true); + Grid.Column refColumnColumn = grid.addColumn(MetaDataColumn::getRefColumn) + .setHeader("Ref Column Name").setResizable(true).setSortable(true); TextField searchField = new TextField(); @Getter MetaDataForm form; @@ -186,7 +188,7 @@ public class MetaDataView extends VerticalLayout { columnToggleContextMenu.addColumnToggleItem(columnLabelColumn.getHeaderText(), columnLabelColumn); columnToggleContextMenu.addColumnToggleItem(showFilterColumn.getHeaderText(), showFilterColumn); columnToggleContextMenu.addColumnToggleItem(filterLabelColumn.getHeaderText(), filterLabelColumn); - + columnToggleContextMenu.addColumnToggleItem(refColumnColumn.getHeaderText(), refColumnColumn); HorizontalLayout toolbar = new HorizontalLayout(searchField, addMetaDataButton, menuButton); toolbar.addClassName("toolbar"); return toolbar; From 3f0a37ff19e29f90a3771ef77ecbc868c79ca58b Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 13 Jan 2025 00:26:42 +0100 Subject: [PATCH 42/91] create files with abstract model class --- qt/database/media.py | 6 +++--- qt/database/tysc.py | 2 +- qt/gui/data_view.py | 12 ++++++++++++ qt/gui/data_view_model.py | 33 +++++++++++++++++++++++++++++++++ qt/gui/table_model.py | 15 ++++++++++----- 5 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 qt/gui/data_view.py create mode 100644 qt/gui/data_view_model.py diff --git a/qt/database/media.py b/qt/database/media.py index 621fdce..b5d793b 100644 --- a/qt/database/media.py +++ b/qt/database/media.py @@ -1,4 +1,4 @@ -from sqlalchemy import Boolean, Column, DateTime, Integer, String +from sqlalchemy import Column, DateTime, Integer, String from sqlalchemy.dialects.mysql import BIT from database.base import Base @@ -13,10 +13,10 @@ class MediaFile(Base): cloud_link = Column(String(255)) file_name = Column(String(255)) path = Column(String(255)) - review = Column(BIT(1), default=True) + review = Column(BIT(1)) title = Column(String(255)) url = Column(String(255)) - should_download = Column(Boolean, default=True) + should_download = Column(BIT(1)) def __repr__(self): return f'MediaFile({self.id} {self.title} {self.title})' diff --git a/qt/database/tysc.py b/qt/database/tysc.py index 9c94177..0e77165 100644 --- a/qt/database/tysc.py +++ b/qt/database/tysc.py @@ -1,4 +1,4 @@ -from sqlalchemy import Boolean, Column, DateTime, Integer, String, ForeignKey, UniqueConstraint +from sqlalchemy import Column, DateTime, Integer, String, ForeignKey, UniqueConstraint from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import relationship diff --git a/qt/gui/data_view.py b/qt/gui/data_view.py new file mode 100644 index 0000000..91c66d2 --- /dev/null +++ b/qt/gui/data_view.py @@ -0,0 +1,12 @@ +from abc import ABC, abstractmethod + + +class DataViewMeta(ABC): + @abstractmethod + def get_header(self): + pass + + +class ComicView(DataViewMeta): + def get_header(self): + pass diff --git a/qt/gui/data_view_model.py b/qt/gui/data_view_model.py new file mode 100644 index 0000000..e4e1bab --- /dev/null +++ b/qt/gui/data_view_model.py @@ -0,0 +1,33 @@ +from typing import List + +from PyQt5.QtCore import QAbstractTableModel +from PySide6.QtCore import QModelIndex +from PySide6.QtGui import Qt + +from gui.data_view import DataViewMeta + + +class DataViewModel(QAbstractTableModel): + def __init__(self): + super().__init__() + self.main_window = None + self._config = None + self._data = List[DataViewMeta] + + def rowCount(self, parent = QModelIndex()): + return len(self._data) + + def columnCount(self, parent = QModelIndex()): + return 0 + + def headerData(self, section, orientation, role = Qt.ItemDataRole.DisplayRole): + return None + + def data(self, index, role = Qt.ItemDataRole.DisplayRole): + return None + + def setData(self, index, value, role = Qt.ItemDataRole.EditRole): + return False + + def flags(self, index): + return None diff --git a/qt/gui/table_model.py b/qt/gui/table_model.py index 5e80c3b..e11dacc 100644 --- a/qt/gui/table_model.py +++ b/qt/gui/table_model.py @@ -45,8 +45,8 @@ class KontorTableModel(QAbstractTableModel): if self._data is None: return None value = self._data[index.row()][index.column()] - if role == Qt.ItemDataRole.DisplayRole: - # print('{}: {}'.format(value, type(value))) + # print('{}:: {}:: {}: {}'.format(index, role, value, type(value))) + if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole: if isinstance(value, datetime): return value.strftime("%Y-%m-%d %M:%M:%S") if isinstance(value, str): @@ -57,6 +57,7 @@ class KontorTableModel(QAbstractTableModel): else: return self._main_window.cross if isinstance(value, int): + print('{}:: {}: {}'.format(index, value, type(value))) if value == 1: return self._main_window.tick else: @@ -69,7 +70,6 @@ class KontorTableModel(QAbstractTableModel): return str(value) if role == Qt.ItemDataRole.DecorationRole: if isinstance(value, bytes): - # print('{}: {}'.format(value, type(value))) if value == b'\x01': return self._main_window.tick else: @@ -91,13 +91,18 @@ class KontorTableModel(QAbstractTableModel): # print(f"Header count: {len(self._config.get_header())}") return len(self._config.header) - def setData(self, index, value, role=Qt.ItemDataRole.EditRole): + def setData(self, index, value, role: int) -> bool: + print(index, role) if role == Qt.ItemDataRole.EditRole: self._data[index.row()][index.column()] = value + print(self._data[index.row()][index.column()]) + self.dataChanged.emit(index, index) + return True if role == Qt.ItemDataRole.CheckStateRole: + print("role == Qt.ItemDataRole.CheckStateRole") checked = value == Qt.CheckState.Checked self._data[index.row()][index.column()] = checked - return True + return False def flags(self, index): return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate From 89b7b87b8cceac37a4557a464dff2ff9919f1459 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 13 Jan 2025 16:18:13 +0100 Subject: [PATCH 43/91] add export to json --- qt/database/__init__.py | 118 +++++++++++++++++++++++++++++++------- qt/database/base.py | 19 +++++- qt/database/comic.py | 8 +-- qt/gui/data_view_model.py | 13 ++--- qt/gui/dialogs.py | 4 +- qt/gui/main_window.py | 9 +-- qt/gui/model_config.py | 31 +++++----- qt/gui/table_model.py | 2 +- 8 files changed, 150 insertions(+), 54 deletions(-) diff --git a/qt/database/__init__.py b/qt/database/__init__.py index c0191cc..d74e927 100644 --- a/qt/database/__init__.py +++ b/qt/database/__init__.py @@ -1,3 +1,7 @@ +import json +from datetime import datetime +from pathlib import Path + import mariadb from sqlalchemy import create_engine, select, text, MetaData, join from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker @@ -30,35 +34,67 @@ class KontorDB: __session__ = sessionmaker(bind=engine) self.session = __session__() - def get_table_id(self, table_name): - result = self.session.execute(select(MetaDataTable.id).where(MetaDataTable.table_name == table_name)).scalar() - return result - def get_table_names(self) -> list: tables = self.session.query(MetaDataTable).all() result = [table.table_name for table in tables] return result - def get_column_meta_data(self, table_id: str, table_name: str) -> dict: + def get_column_meta_data(self, table_name: str, view_only=True) -> dict: meta_data = {} order = 0 - for (_, column) in self.session.query(MetaDataTable, MetaDataColumn).filter(MetaDataTable.id == MetaDataColumn.table_id).filter(MetaDataTable.table_name == table_name).filter(MetaDataColumn.is_shown == 1).all(): - meta_data[order] = {'column': column.column_name, 'label': column.column_label, 'order': column.column_order, 'ref_column': column.ref_column} - order += 1 + if view_only: + for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name). + filter(MetaDataColumn.is_shown == 1).all()): + meta_data[order] = {'column': column.column_name, 'label': column.column_label, + 'order': column.column_order, 'ref_column': column.ref_column} + order += 1 + else: + for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name).all()): + meta_data[order] = { + 'column': column.column_name, + 'order': column.column_order, + 'ref_column': column.ref_column + } + order += 1 return meta_data - def get_filters(self, table_id): - cursor = self.db_conn.cursor() - filters = {} - cursor.execute( - "SELECT column_name, filter_label from meta_data_column WHERE table_id=? AND show_filter is true", - (table_id,)) - rows = cursor.fetchall() - for row in rows: - filters[row[0]] = {'label': row[1], 'widget': None} - cursor.close() - # print(f"retrieved {len(rows)} filters: {filters}") - return filters + def get_filters(self, table_name): + _filter_map = {} + for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name). + filter(MetaDataColumn.show_filter == 1).all()): + _filter_map[column.column_name] = {'label': column.filter_label, 'widget': None} + print(f"retrieved {len(_filter_map)} filters: {_filter_map}") + return _filter_map + + def data(self, table, columns: dict, filters) -> list: + data = [] + entries = [] + if len(filters) == 0: + entries = self.session.query(table).all() + else: + entries = self.session.query(table).filter_by(**filters) + for entry in entries: + row = [] + for order in columns.keys(): + column_name = columns[order]['column'] + if str(column_name).endswith("_id"): + ref_table = column_name[:-3] + # print(f"{ref_table=}") + ref = getattr(entry, ref_table) + value = getattr(ref, "name") + # print(f"{value=}") + row.append(value) + else: + row.append(getattr(entry, column_name)) + # print(repr(row)) + data.append(row) + return data def get_data(self, table_name: str, columns: dict, where_clause: str) -> list: data = [] @@ -103,3 +139,45 @@ class KontorDB: statement = f"SELECT {columns} FROM {table} {where_clause}" print(f"{statement=}") return statement + + def export_db(self, export_type: str, export_file_name: str, export_table_list: list): + print(f"export DB to {export_file_name} as {export_type}") + db = {} + for table in export_table_list: + columns = self.get_column_meta_data(table, view_only=False) + model = Base.model_lookup_by_table_name(table) + rows = self.session.query(model).all() + entries = [] + print(f"found {len(rows)} entries") + print(f"found {len(columns)} columns") + for row in rows: + print(row) + entry = {} + for order in columns: + print(columns[order]) + column_name = columns[order]['column'] + print(f"get value {column_name} from {row} of table {table}") + try: + value = getattr(row, column_name) + if isinstance(value, datetime): + entry[column_name] = str(value) + else: + entry[column_name] = value + except AttributeError as error: + print("could not get value") + entries.append(entry) + db[table] = entries + export_file = Path(export_file_name) + match export_type: + case "JSON": + json_dump = json.dumps(db, indent=4) + with open(export_file_name, "w") as dump_file: + dump_file.write(json_dump) + case "YAML": + export_file = Path(export_file_name) + case "SQLite": + export_file = Path(export_file_name) + case _: + print("unknown export type") + if export_file.exists(): + print(f"{export_file} exists") diff --git a/qt/database/base.py b/qt/database/base.py index 9339e51..f97d724 100644 --- a/qt/database/base.py +++ b/qt/database/base.py @@ -1,7 +1,20 @@ from sqlalchemy import Column, String, DateTime, Integer -from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker +from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker, declarative_base -class Base(DeclarativeBase): - pass +# class Base(DeclarativeBase): +# pass +class BaseModel: + + @classmethod + def model_lookup_by_table_name(cls, table_name): + registry_instance = getattr(cls, "registry") + for mapper_ in registry_instance.mappers: + model = mapper_.class_ + model_class_name = model.__tablename__ + if model_class_name == table_name: + return model + + +Base = declarative_base(cls=BaseModel) \ No newline at end of file diff --git a/qt/database/comic.py b/qt/database/comic.py index dc4204f..1dc31b4 100644 --- a/qt/database/comic.py +++ b/qt/database/comic.py @@ -1,4 +1,4 @@ -from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import relationship @@ -106,7 +106,7 @@ class Artist(Base): comic_works = relationship("ComicWork") -class Worktype(Base): +class WorkType(Base): __tablename__ = "worktype" id = Column(String, primary_key=True) created_date = Column(DateTime) @@ -126,5 +126,5 @@ class ComicWork(Base): comic = relationship("Comic", back_populates="comic_works") artist_id = Column(String, ForeignKey("artist.id"), nullable=False) artist = relationship("Artist", back_populates="comic_works") - worktype_id = Column(String, ForeignKey("worktype.id"), nullable=False) - worktype = relationship("Worktype", back_populates="comic_works") + work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False) + work_type = relationship("WorkType", back_populates="comic_works") diff --git a/qt/gui/data_view_model.py b/qt/gui/data_view_model.py index e4e1bab..4ffdc8c 100644 --- a/qt/gui/data_view_model.py +++ b/qt/gui/data_view_model.py @@ -1,7 +1,6 @@ from typing import List -from PyQt5.QtCore import QAbstractTableModel -from PySide6.QtCore import QModelIndex +from PySide6.QtCore import QModelIndex, QAbstractTableModel from PySide6.QtGui import Qt from gui.data_view import DataViewMeta @@ -14,19 +13,19 @@ class DataViewModel(QAbstractTableModel): self._config = None self._data = List[DataViewMeta] - def rowCount(self, parent = QModelIndex()): + def rowCount(self, parent=QModelIndex()): return len(self._data) - def columnCount(self, parent = QModelIndex()): + def columnCount(self, parent=QModelIndex()): return 0 - def headerData(self, section, orientation, role = Qt.ItemDataRole.DisplayRole): + def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole): return None - def data(self, index, role = Qt.ItemDataRole.DisplayRole): + def data(self, index, role=Qt.ItemDataRole.DisplayRole): return None - def setData(self, index, value, role = Qt.ItemDataRole.EditRole): + def setData(self, index, value, role=Qt.ItemDataRole.EditRole): return False def flags(self, index): diff --git a/qt/gui/dialogs.py b/qt/gui/dialogs.py index 703baeb..21dbe92 100644 --- a/qt/gui/dialogs.py +++ b/qt/gui/dialogs.py @@ -1,7 +1,7 @@ from pathlib import Path from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QFileDialog, \ - QGroupBox, QCheckBox, QComboBox + QCheckBox, QComboBox class ExportKontorDialog(QDialog): @@ -10,7 +10,7 @@ class ExportKontorDialog(QDialog): self.parent = parent self.kontor_db = kontor_db - self.file_name = None + self.file_name = "data.json" self.tables = [] self._table_options = {} diff --git a/qt/gui/main_window.py b/qt/gui/main_window.py index 020326f..66f2302 100644 --- a/qt/gui/main_window.py +++ b/qt/gui/main_window.py @@ -34,8 +34,8 @@ class MainWindow(QMainWindow): parent_layout = QVBoxLayout() self.central_widget.setLayout(parent_layout) self.tabs = QTabWidget() - self.tabs.addTab(self.generate_data_tab("comic"), "Comics") - self.tabs.addTab(self.generate_data_tab("media_file"), "MediaFile") + self.tabs.addTab(self.generate_data_tab("comic", Comic), "Comics") + self.tabs.addTab(self.generate_data_tab("media_file", MediaFile), "MediaFile") self.tabs.currentChanged.connect(self._tab_changed) #label.setAlignment(Qt.AlignmentFlag.AlignCenter) parent_layout.addWidget(self.tabs) @@ -112,6 +112,7 @@ class MainWindow(QMainWindow): print(export_dlg.get_tables_to_export()) print(f"export DB to {export_dlg.file_name}") self.statusBar.showMessage(f"export DB to {export_dlg.file_name}", 3000) + self.kontor_db.export_db(export_dlg.current_export_type, export_dlg.file_name, export_dlg.get_tables_to_export()) else: self.statusBar.showMessage("Export cancelled", 3000) @@ -121,9 +122,9 @@ class MainWindow(QMainWindow): def _tab_changed(self, tab_index): self.data[tab_index].refresh() - def generate_data_tab(self, table_name): + def generate_data_tab(self, table_name, table): data_tab = QWidget() - table_config = KontorModelConfig(self.kontor_db, self, table_name) + table_config = KontorModelConfig(self.kontor_db, self, table_name, table) model = KontorTableModel(table_config) layout = QVBoxLayout() self.data.append(model) diff --git a/qt/gui/model_config.py b/qt/gui/model_config.py index c2644e9..a3f902d 100644 --- a/qt/gui/model_config.py +++ b/qt/gui/model_config.py @@ -6,25 +6,18 @@ from database import KontorDB class KontorModelConfig: - def __init__(self, kontor_db: KontorDB, main_window, table_name: str): + def __init__(self, kontor_db: KontorDB, main_window, table_name: str, table): self.header = {} self.filter = {} self.main_window = main_window - self._table = table_name - self._table_id = None + self._table_name = table_name + self._table = table self.kontor_db = kontor_db self.get_table_config() - def get_table_id(self): - if self._table_id is not None: - return - self._table_id = self.kontor_db.get_table_id(self._table) - def get_table_config(self): - if self._table_id is None: - self.get_table_id() - self.header = self.kontor_db.get_column_meta_data(self._table_id, self._table) - self.filter = self.kontor_db.get_filters(self._table_id) + self.header = self.kontor_db.get_column_meta_data(self._table_name) + self.filter = self.kontor_db.get_filters(self._table_name) def get_filter(self) -> str: filter_rule = "" @@ -41,8 +34,20 @@ class KontorModelConfig: # print(f"{filter_rule=}") return filter_rule + def filters(self) -> dict: + _filters = {} + # print(self.filter["download"].isChecked()) + for column, filter_info in self.filter.items(): + # print(column, filter_info) + if filter_info['widget'].isChecked(): + _filters[column] = True + # print(f"{filter_rule=}") + return _filters + def get_data(self) -> list: - data = self.kontor_db.get_data(self._table, self.header, self.get_filter()) + # data = self.kontor_db.get_data(self._table_name, self.header, self.get_filter()) + # data.clear() + data = self.kontor_db.data(self._table, self.header, self.filters()) # print(f"KontorModelConfig.get_data: {len(data)}") # comics = self.kontor_db.session.query(Comic).all() # print(f'{len(comics)} Comics loaded') diff --git a/qt/gui/table_model.py b/qt/gui/table_model.py index e11dacc..e1ba6ae 100644 --- a/qt/gui/table_model.py +++ b/qt/gui/table_model.py @@ -57,7 +57,7 @@ class KontorTableModel(QAbstractTableModel): else: return self._main_window.cross if isinstance(value, int): - print('{}:: {}: {}'.format(index, value, type(value))) + # print('{}:: {}: {}'.format(index, value, type(value))) if value == 1: return self._main_window.tick else: From 54bc17ee7d34a83867e96d941fd6976e29e96f17 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 13 Jan 2025 22:54:25 +0100 Subject: [PATCH 44/91] add cli app and fix relationship typos --- python/cli/.gitignore | 105 +++++++++ python/cli/CHANGELOG.md | 5 + python/cli/Dockerfile | 10 + python/cli/LICENSE.md | 1 + python/cli/MANIFEST.in | 5 + python/cli/Makefile | 31 +++ python/cli/README.md | 69 ++++++ python/cli/config/kontor.yml.example | 46 ++++ .../__init__.py => python/cli/docs/.gitkeep | 0 python/cli/kontor/__init__.py | 0 python/cli/kontor/controllers/__init__.py | 0 python/cli/kontor/controllers/clibase.py | 60 +++++ python/cli/kontor/core/__init__.py | 0 python/cli/kontor/core/exc.py | 4 + python/cli/kontor/core/version.py | 7 + .../cli/kontor}/database/__init__.py | 6 +- {qt => python/cli/kontor}/database/base.py | 0 {qt => python/cli/kontor}/database/comic.py | 2 +- python/cli/kontor/database/media.py | 25 +++ python/cli/kontor/database/metadata.py | 49 ++++ {qt => python/cli/kontor}/database/tysc.py | 2 +- python/cli/kontor/ext/__init__.py | 0 python/cli/kontor/main.py | 111 +++++++++ python/cli/kontor/plugins/__init__.py | 0 python/cli/kontor/templates/__init__.py | 0 python/cli/kontor/templates/command1.jinja2 | 4 + python/cli/requirements-dev.txt | 8 + python/cli/requirements.txt | 6 + python/cli/setup.cfg | 0 python/cli/setup.py | 28 +++ python/cli/tests/conftest.py | 16 ++ python/cli/tests/test_kontor.py | 36 +++ python/main.py | 16 ++ {qt => python/qt}/.gitignore | 0 python/qt/database/__init__.py | 212 ++++++++++++++++++ python/qt/database/base.py | 18 ++ python/qt/database/comic.py | 130 +++++++++++ {qt => python/qt}/database/media.py | 0 {qt => python/qt}/database/metadata.py | 0 python/qt/database/tysc.py | 131 +++++++++++ python/qt/gui/__init__.py | 0 {qt => python/qt}/gui/data_view.py | 0 {qt => python/qt}/gui/data_view_model.py | 0 {qt => python/qt}/gui/dialogs.py | 0 {qt => python/qt}/gui/main_window.py | 0 {qt => python/qt}/gui/model_config.py | 0 {qt => python/qt}/gui/table_model.py | 0 {qt => python/qt}/kontor.py | 0 {qt => python/qt}/pysidedeploy.spec | 0 {qt => python/qt}/res/application-export.png | Bin {qt => python/qt}/res/application-import.png | Bin {qt => python/qt}/res/arrow-circle-double.png | Bin {qt => python/qt}/res/cross.png | Bin {qt => python/qt}/res/tick.png | Bin 54 files changed, 1138 insertions(+), 5 deletions(-) create mode 100644 python/cli/.gitignore create mode 100644 python/cli/CHANGELOG.md create mode 100644 python/cli/Dockerfile create mode 100644 python/cli/LICENSE.md create mode 100644 python/cli/MANIFEST.in create mode 100644 python/cli/Makefile create mode 100644 python/cli/README.md create mode 100644 python/cli/config/kontor.yml.example rename qt/gui/__init__.py => python/cli/docs/.gitkeep (100%) create mode 100644 python/cli/kontor/__init__.py create mode 100644 python/cli/kontor/controllers/__init__.py create mode 100644 python/cli/kontor/controllers/clibase.py create mode 100644 python/cli/kontor/core/__init__.py create mode 100644 python/cli/kontor/core/exc.py create mode 100644 python/cli/kontor/core/version.py rename {qt => python/cli/kontor}/database/__init__.py (98%) rename {qt => python/cli/kontor}/database/base.py (100%) rename {qt => python/cli/kontor}/database/comic.py (99%) create mode 100644 python/cli/kontor/database/media.py create mode 100644 python/cli/kontor/database/metadata.py rename {qt => python/cli/kontor}/database/tysc.py (99%) create mode 100644 python/cli/kontor/ext/__init__.py create mode 100644 python/cli/kontor/main.py create mode 100644 python/cli/kontor/plugins/__init__.py create mode 100644 python/cli/kontor/templates/__init__.py create mode 100644 python/cli/kontor/templates/command1.jinja2 create mode 100644 python/cli/requirements-dev.txt create mode 100644 python/cli/requirements.txt create mode 100644 python/cli/setup.cfg create mode 100644 python/cli/setup.py create mode 100644 python/cli/tests/conftest.py create mode 100644 python/cli/tests/test_kontor.py create mode 100644 python/main.py rename {qt => python/qt}/.gitignore (100%) create mode 100644 python/qt/database/__init__.py create mode 100644 python/qt/database/base.py create mode 100644 python/qt/database/comic.py rename {qt => python/qt}/database/media.py (100%) rename {qt => python/qt}/database/metadata.py (100%) create mode 100644 python/qt/database/tysc.py create mode 100644 python/qt/gui/__init__.py rename {qt => python/qt}/gui/data_view.py (100%) rename {qt => python/qt}/gui/data_view_model.py (100%) rename {qt => python/qt}/gui/dialogs.py (100%) rename {qt => python/qt}/gui/main_window.py (100%) rename {qt => python/qt}/gui/model_config.py (100%) rename {qt => python/qt}/gui/table_model.py (100%) rename {qt => python/qt}/kontor.py (100%) rename {qt => python/qt}/pysidedeploy.spec (100%) rename {qt => python/qt}/res/application-export.png (100%) rename {qt => python/qt}/res/application-import.png (100%) rename {qt => python/qt}/res/arrow-circle-double.png (100%) rename {qt => python/qt}/res/cross.png (100%) rename {qt => python/qt}/res/tick.png (100%) diff --git a/python/cli/.gitignore b/python/cli/.gitignore new file mode 100644 index 0000000..a74b246 --- /dev/null +++ b/python/cli/.gitignore @@ -0,0 +1,105 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +coverage-report/ +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/python/cli/CHANGELOG.md b/python/cli/CHANGELOG.md new file mode 100644 index 0000000..6c95d97 --- /dev/null +++ b/python/cli/CHANGELOG.md @@ -0,0 +1,5 @@ +# Kontor Change History + +## 0.0.1 + +Initial release. diff --git a/python/cli/Dockerfile b/python/cli/Dockerfile new file mode 100644 index 0000000..d32cf5c --- /dev/null +++ b/python/cli/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.9-alpine +LABEL MAINTAINER="Thomas Peetz " +ENV PS1="\[\e[0;33m\]|> kontor <| \[\e[1;35m\]\W\[\e[0m\] \[\e[0m\]# " + +WORKDIR /src +COPY . /src +RUN pip install --no-cache-dir -r requirements.txt \ + && python setup.py install +WORKDIR / +ENTRYPOINT ["kontor"] diff --git a/python/cli/LICENSE.md b/python/cli/LICENSE.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/python/cli/LICENSE.md @@ -0,0 +1 @@ + diff --git a/python/cli/MANIFEST.in b/python/cli/MANIFEST.in new file mode 100644 index 0000000..1160952 --- /dev/null +++ b/python/cli/MANIFEST.in @@ -0,0 +1,5 @@ +recursive-include *.py +include setup.cfg +include README.md CHANGELOG.md LICENSE.md +include *.txt +recursive-include kontor/templates * diff --git a/python/cli/Makefile b/python/cli/Makefile new file mode 100644 index 0000000..b016c3c --- /dev/null +++ b/python/cli/Makefile @@ -0,0 +1,31 @@ +.PHONY: clean virtualenv test docker dist dist-upload + +clean: + find . -name '*.py[co]' -delete + +virtualenv: + virtualenv --prompt '|> kontor <| ' env + env/bin/pip install -r requirements-dev.txt + env/bin/python setup.py develop + @echo + @echo "VirtualENV Setup Complete. Now run: source env/bin/activate" + @echo + +test: + python -m pytest \ + -v \ + --cov=kontor \ + --cov-report=term \ + --cov-report=html:coverage-report \ + tests/ + +docker: clean + docker build -t kontor:latest . + +dist: clean + rm -rf dist/* + python setup.py sdist + python setup.py bdist_wheel + +dist-upload: + twine upload dist/* diff --git a/python/cli/README.md b/python/cli/README.md new file mode 100644 index 0000000..6afe593 --- /dev/null +++ b/python/cli/README.md @@ -0,0 +1,69 @@ +# Kontor CLI Tool + +## Installation + +``` +$ pip install -r requirements.txt + +$ python setup.py install +``` + +## Development + +This project includes a number of helpers in the `Makefile` to streamline common development tasks. + +### Environment Setup + +The following demonstrates setting up and working with a development environment: + +``` +### create a virtualenv for development + +$ make virtualenv + +$ source env/bin/activate + + +### run kontor cli application + +$ kontor --help + + +### run pytest / coverage + +$ make test +``` + + +### Releasing to PyPi + +Before releasing to PyPi, you must configure your login credentials: + +**~/.pypirc**: + +``` +[pypi] +username = YOUR_USERNAME +password = YOUR_PASSWORD +``` + +Then use the included helper function via the `Makefile`: + +``` +$ make dist + +$ make dist-upload +``` + +## Deployments + +### Docker + +Included is a basic `Dockerfile` for building and distributing `Kontor`, +and can be built with the included `make` helper: + +``` +$ make docker + +$ docker run -it kontor --help +``` diff --git a/python/cli/config/kontor.yml.example b/python/cli/config/kontor.yml.example new file mode 100644 index 0000000..ba6507c --- /dev/null +++ b/python/cli/config/kontor.yml.example @@ -0,0 +1,46 @@ +### Kontor Configuration Settings +--- + +kontor: + +### Toggle application level debug (does not toggle framework debugging) +# debug: false + +### Where external (third-party) plugins are loaded from +# plugin_dir: /var/lib/kontor/plugins/ + +### Where all plugin configurations are loaded from +# plugin_config_dir: /etc/kontor/plugins.d/ + +### Where external templates are loaded from +# template_dir: /var/lib/kontor/templates/ + +### The log handler label +# log_handler: colorlog + +### The output handler label +# output_handler: jinja2 + +### sample foo option +# foo: bar + + +log.colorlog: + +### Where the log file lives (no log file by default) +# file: null + +### The level for which to log. One of: info, warning, error, fatal, debug +# level: info + +### Whether or not to log to console +# to_console: true + +### Whether or not to rotate the log file when it reaches `max_bytes` +# rotate: false + +### Max size in bytes that a log file can grow until it is rotated. +# max_bytes: 512000 + +### The maximum number of log files to maintain when rotating +# max_files: 4 diff --git a/qt/gui/__init__.py b/python/cli/docs/.gitkeep similarity index 100% rename from qt/gui/__init__.py rename to python/cli/docs/.gitkeep diff --git a/python/cli/kontor/__init__.py b/python/cli/kontor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/kontor/controllers/__init__.py b/python/cli/kontor/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/kontor/controllers/clibase.py b/python/cli/kontor/controllers/clibase.py new file mode 100644 index 0000000..8d69fb2 --- /dev/null +++ b/python/cli/kontor/controllers/clibase.py @@ -0,0 +1,60 @@ + +from cement import Controller, ex +from cement.utils.version import get_version_banner +from ..core.version import get_version + +VERSION_BANNER = """ +Kontor CLI Tool %s +%s +""" % (get_version(), get_version_banner()) + + +class CliBase(Controller): + class Meta: + label = 'base' + + # text displayed at the top of --help output + description = 'Kontor CLI Tool' + + # text displayed at the bottom of --help output + epilog = 'Usage: kontor command1 --foo bar' + + # controller level arguments. ex: 'kontor --version' + arguments = [ + ### add a version banner + ( [ '-v', '--version' ], + { 'action' : 'version', + 'version' : VERSION_BANNER } ), + ] + + + def _default(self): + """Default action if no sub-command is passed.""" + + self.app.args.print_help() + + + @ex( + help='example sub command1', + + # sub-command level arguments. ex: 'kontor command1 --foo bar' + arguments=[ + ### add a sample foo option under subcommand namespace + ( [ '-f', '--foo' ], + { 'help' : 'notorious foo option', + 'action' : 'store', + 'dest' : 'foo' } ), + ], + ) + def command1(self): + """Example sub-command.""" + + data = { + 'foo' : 'bar', + } + + ### do something with arguments + if self.app.pargs.foo is not None: + data['foo'] = self.app.pargs.foo + + self.app.render(data, 'command1.jinja2') diff --git a/python/cli/kontor/core/__init__.py b/python/cli/kontor/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/kontor/core/exc.py b/python/cli/kontor/core/exc.py new file mode 100644 index 0000000..aaeb159 --- /dev/null +++ b/python/cli/kontor/core/exc.py @@ -0,0 +1,4 @@ + +class KontorError(Exception): + """Generic errors.""" + pass diff --git a/python/cli/kontor/core/version.py b/python/cli/kontor/core/version.py new file mode 100644 index 0000000..d130f85 --- /dev/null +++ b/python/cli/kontor/core/version.py @@ -0,0 +1,7 @@ + +from cement.utils.version import get_version as cement_get_version + +VERSION = (0, 0, 1, 'alpha', 0) + +def get_version(version=VERSION): + return cement_get_version(version) diff --git a/qt/database/__init__.py b/python/cli/kontor/database/__init__.py similarity index 98% rename from qt/database/__init__.py rename to python/cli/kontor/database/__init__.py index d74e927..95ce1d8 100644 --- a/qt/database/__init__.py +++ b/python/cli/kontor/database/__init__.py @@ -6,9 +6,9 @@ import mariadb from sqlalchemy import create_engine, select, text, MetaData, join from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker -from database.base import Base -from database.comic import Comic -from database.metadata import MetaDataTable, MetaDataColumn +from .base import Base +from .comic import Comic +from .metadata import MetaDataTable, MetaDataColumn class KontorDB: diff --git a/qt/database/base.py b/python/cli/kontor/database/base.py similarity index 100% rename from qt/database/base.py rename to python/cli/kontor/database/base.py diff --git a/qt/database/comic.py b/python/cli/kontor/database/comic.py similarity index 99% rename from qt/database/comic.py rename to python/cli/kontor/database/comic.py index 1dc31b4..49d222f 100644 --- a/qt/database/comic.py +++ b/python/cli/kontor/database/comic.py @@ -2,7 +2,7 @@ from sqlalchemy import Column, DateTime, ForeignKey, Integer, String from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import relationship -from database.base import Base +from .base import Base class Publisher(Base): diff --git a/python/cli/kontor/database/media.py b/python/cli/kontor/database/media.py new file mode 100644 index 0000000..0129c03 --- /dev/null +++ b/python/cli/kontor/database/media.py @@ -0,0 +1,25 @@ +from sqlalchemy import Column, DateTime, Integer, String +from sqlalchemy.dialects.mysql import BIT + +from .base import Base + + +class MediaFile(Base): + __tablename__ = 'media_file' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + cloud_link = Column(String(255)) + file_name = Column(String(255)) + path = Column(String(255)) + review = Column(BIT(1)) + title = Column(String(255)) + url = Column(String(255)) + should_download = Column(BIT(1)) + + def __repr__(self): + return f'MediaFile({self.id} {self.title} {self.title})' + + def __str__(self): + return f'{self.title}({self.id})' diff --git a/python/cli/kontor/database/metadata.py b/python/cli/kontor/database/metadata.py new file mode 100644 index 0000000..2975e10 --- /dev/null +++ b/python/cli/kontor/database/metadata.py @@ -0,0 +1,49 @@ +from sqlalchemy import Column, String, ForeignKey, DateTime, Integer, Boolean +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from .base import Base + + +class MetaDataTable(Base): + __tablename__ = 'meta_data_table' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + table_name = Column(String(255), unique=True) + table_columns = relationship("MetaDataColumn") + + def __repr__(self): + return f'MetaDataTable({self.id} {self.table_name})' + + def __str__(self): + return f'{self.table_name}({self.id})' + +class MetaDataColumn(Base): + __tablename__ = 'meta_data_column' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + column_modifier = Column(String(255), nullable=True) + column_name = Column(String(255)) + column_order = Column(Integer) + column_sync_name = Column(String(255)) + column_type = Column(String(255)) + table_id = Column(String, ForeignKey('meta_data_table.id')) + table = relationship("MetaDataTable", back_populates="table_columns") + column_label = Column(String(255)) + filter_label = Column(String(255)) + is_shown = Column(BIT(1)) + show_filter = Column(BIT(1)) + ref_column = Column(String, nullable=True) + + def __repr__(self): + if self.column_name is None: + return f'MetaDataColumn({self.id} {self.table.table_name}.__)' + else: + return f'MetaDataColumn({self.id} {self.table.table_name}.{self.column_name})' + + def __str__(self): + return f'{self.column_name}({self.id})' diff --git a/qt/database/tysc.py b/python/cli/kontor/database/tysc.py similarity index 99% rename from qt/database/tysc.py rename to python/cli/kontor/database/tysc.py index 0e77165..54cacea 100644 --- a/qt/database/tysc.py +++ b/python/cli/kontor/database/tysc.py @@ -2,7 +2,7 @@ from sqlalchemy import Column, DateTime, Integer, String, ForeignKey, UniqueCons from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import relationship -from database.base import Base +from .base import Base class Sport(Base): diff --git a/python/cli/kontor/ext/__init__.py b/python/cli/kontor/ext/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/kontor/main.py b/python/cli/kontor/main.py new file mode 100644 index 0000000..4c71aab --- /dev/null +++ b/python/cli/kontor/main.py @@ -0,0 +1,111 @@ + +from cement import App, TestApp, init_defaults +from cement.core.exc import CaughtSignal +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from .core.exc import KontorError +from .database.base import Base +from .controllers.clibase import CliBase + +# configuration defaults +CONFIG = init_defaults('kontor', 'mariadb') +CONFIG['kontor']['foo'] = 'bar' +CONFIG['mariadb']['user'] = 'kontor' +CONFIG['mariadb']['password'] = 'kontor' +CONFIG['mariadb']['host'] = '127.0.0.1' +CONFIG['mariadb']['port'] = '3306' +CONFIG['mariadb']['database'] = 'kontor' + +def extend_sqlalchemy(app): + app.log.info('extending kontor application with sqlalchemy') + connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format( + app.config.get('mariadb', 'user'), + app.config.get('mariadb', 'password'), + app.config.get('mariadb', 'host'), + app.config.get('mariadb', 'port'), + app.config.get('mariadb', 'database') + )) + # engine = create_engine(connect_string, echo=True) + engine = create_engine(connect_string) + Base.metadata.create_all(bind=engine) + __session__ = sessionmaker(bind=engine) + app.extend('session', __session__()) + + +class Kontor(App): + """Kontor primary application.""" + + class Meta: + label = 'kontor' + + # configuration defaults + config_defaults = CONFIG + + # call sys.exit() on close + exit_on_close = True + + # load additional framework extensions + extensions = [ + 'yaml', + 'colorlog', + 'jinja2', + ] + + # configuration handler + config_handler = 'yaml' + + # configuration file suffix + config_file_suffix = '.yml' + + # set the log handler + log_handler = 'colorlog' + + # set the output handler + output_handler = 'jinja2' + + hooks = [ + ('post_setup', extend_sqlalchemy), + ] + # register handlers + handlers = [ + CliBase + ] + + +class KontorTest(TestApp,Kontor): + """A sub-class of Kontor that is better suited for testing.""" + + class Meta: + label = 'kontor' + + +def main(): + with Kontor() as app: + try: + app.run() + + except AssertionError as e: + print('AssertionError > %s' % e.args[0]) + app.exit_code = 1 + + if app.debug is True: + import traceback + traceback.print_exc() + + except KontorError as e: + print('KontorError > %s' % e.args[0]) + app.exit_code = 1 + + if app.debug is True: + import traceback + traceback.print_exc() + + except CaughtSignal as e: + # Default Cement signals are SIGINT and SIGTERM, exit 0 (non-error) + print('\n%s' % e) + app.exit_code = 0 + + +if __name__ == '__main__': + main() diff --git a/python/cli/kontor/plugins/__init__.py b/python/cli/kontor/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/kontor/templates/__init__.py b/python/cli/kontor/templates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/kontor/templates/command1.jinja2 b/python/cli/kontor/templates/command1.jinja2 new file mode 100644 index 0000000..2435e4d --- /dev/null +++ b/python/cli/kontor/templates/command1.jinja2 @@ -0,0 +1,4 @@ + +Example Template (templates/command1.jinja2) + +Foo => {{ foo }} diff --git a/python/cli/requirements-dev.txt b/python/cli/requirements-dev.txt new file mode 100644 index 0000000..f20606e --- /dev/null +++ b/python/cli/requirements-dev.txt @@ -0,0 +1,8 @@ +-r requirements.txt + +pytest +pytest-cov +coverage +twine>=1.11.0 +setuptools>=38.6.0 +wheel>=0.31.0 diff --git a/python/cli/requirements.txt b/python/cli/requirements.txt new file mode 100644 index 0000000..bee2df1 --- /dev/null +++ b/python/cli/requirements.txt @@ -0,0 +1,6 @@ +cement==3.0.12 +cement[jinja2] +cement[yaml] +cement[colorlog] +mariadb +sqlalchemy diff --git a/python/cli/setup.cfg b/python/cli/setup.cfg new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/setup.py b/python/cli/setup.py new file mode 100644 index 0000000..0b92a43 --- /dev/null +++ b/python/cli/setup.py @@ -0,0 +1,28 @@ + +from setuptools import setup, find_packages +from kontor.core.version import get_version + +VERSION = get_version() + +f = open('README.md', 'r') +LONG_DESCRIPTION = f.read() +f.close() + +setup( + name='kontor', + version=VERSION, + description='Kontor CLI Tool', + long_description=LONG_DESCRIPTION, + long_description_content_type='text/markdown', + author='Thomas Peetz', + author_email='thomas.peetz@thpeetz.de', + url='https://gitlab.com/tpeetz/kontor', + license='MIT', + packages=find_packages(exclude=['ez_setup', 'tests*']), + package_data={'kontor': ['templates/*']}, + include_package_data=True, + entry_points=""" + [console_scripts] + kontor = kontor.main:main + """, +) diff --git a/python/cli/tests/conftest.py b/python/cli/tests/conftest.py new file mode 100644 index 0000000..5124e2e --- /dev/null +++ b/python/cli/tests/conftest.py @@ -0,0 +1,16 @@ +""" +PyTest Fixtures. +""" + +import pytest +from cement import fs + +@pytest.fixture(scope="function") +def tmp(request): + """ + Create a `tmp` object that geneates a unique temporary directory, and file + for each test function that requires it. + """ + t = fs.Tmp() + yield t + t.remove() diff --git a/python/cli/tests/test_kontor.py b/python/cli/tests/test_kontor.py new file mode 100644 index 0000000..3c1bd67 --- /dev/null +++ b/python/cli/tests/test_kontor.py @@ -0,0 +1,36 @@ + +from pytest import raises +from kontor.main import KontorTest + +def test_kontor(): + # test kontor without any subcommands or arguments + with KontorTest() as app: + app.run() + assert app.exit_code == 0 + + +def test_kontor_debug(): + # test that debug mode is functional + argv = ['--debug'] + with KontorTest(argv=argv) as app: + app.run() + assert app.debug is True + + +def test_command1(): + # test command1 without arguments + argv = ['command1'] + with KontorTest(argv=argv) as app: + app.run() + data,output = app.last_rendered + assert data['foo'] == 'bar' + assert output.find('Foo => bar') + + + # test command1 with arguments + argv = ['command1', '--foo', 'not-bar'] + with KontorTest(argv=argv) as app: + app.run() + data,output = app.last_rendered + assert data['foo'] == 'not-bar' + assert output.find('Foo => not-bar') diff --git a/python/main.py b/python/main.py new file mode 100644 index 0000000..76d0e8c --- /dev/null +++ b/python/main.py @@ -0,0 +1,16 @@ +# This is a sample Python script. + +# Press Umschalt+F10 to execute it or replace it with your code. +# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings. + + +def print_hi(name): + # Use a breakpoint in the code line below to debug your script. + print(f'Hi, {name}') # Press Strg+F8 to toggle the breakpoint. + + +# Press the green button in the gutter to run the script. +if __name__ == '__main__': + print_hi('PyCharm') + +# See PyCharm help at https://www.jetbrains.com/help/pycharm/ diff --git a/qt/.gitignore b/python/qt/.gitignore similarity index 100% rename from qt/.gitignore rename to python/qt/.gitignore diff --git a/python/qt/database/__init__.py b/python/qt/database/__init__.py new file mode 100644 index 0000000..47f5706 --- /dev/null +++ b/python/qt/database/__init__.py @@ -0,0 +1,212 @@ +import json +from datetime import datetime +from pathlib import Path + +import mariadb +from sqlalchemy import create_engine, select, text, MetaData, join +from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker + +from .base import Base +from .comic import Comic, Artist, Publisher, ComicWork, WorkType, StoryArc, Volume, Issue, TradePaperback +from .tysc import Sport, Team, Card, CardSet, Vendor, Rooster, Player, FieldPosition +from .media import MediaFile +from .metadata import MetaDataTable, MetaDataColumn + + +class KontorDB: + + def __init__(self, db_config): + self.db_conn = mariadb.connect( + host=db_config['mariadb']['host'], + port=db_config['mariadb']['port'], + user=db_config['mariadb']['user'], + password=db_config['mariadb']['password'], + database=db_config['mariadb']['database'] + ) + connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}' + .format( + db_config['mariadb']['user'], + db_config['mariadb']['password'], + db_config['mariadb']['host'], + db_config['mariadb']['port'], + db_config['mariadb']['database'])) + # engine = create_engine(connect_string, echo=True) + engine = create_engine(connect_string) + Base.metadata.create_all(bind=engine) + __session__ = sessionmaker(bind=engine) + self.session = __session__() + self.registry = {} + self.init_registry() + + def init_registry(self): + self.registry['card'] = Card + self.registry['card_set'] = CardSet + self.registry['sport'] = Sport + self.registry['team'] = Team + self.registry['field_position'] = FieldPosition + self.registry['rooster'] = Rooster + self.registry['player'] = Player + self.registry['vendor'] = Vendor + self.registry['artist'] = Artist + self.registry['publisher'] = Publisher + self.registry['comic'] = Comic + self.registry['issue'] = Issue + self.registry['story_arc'] = StoryArc + self.registry['trade_paperback'] = TradePaperback + self.registry['volume'] = Volume + self.registry['comic_work'] = ComicWork + self.registry['worktype'] = WorkType + self.registry['media_file'] = MediaFile + + + def get_table_names(self) -> list: + tables = self.session.query(MetaDataTable).all() + result = [table.table_name for table in tables] + return result + + def get_column_meta_data(self, table_name: str, view_only=True) -> dict: + meta_data = {} + order = 0 + if view_only: + for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name). + filter(MetaDataColumn.is_shown == 1).all()): + meta_data[order] = {'column': column.column_name, 'label': column.column_label, + 'order': column.column_order, 'ref_column': column.ref_column} + order += 1 + else: + for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name).all()): + meta_data[order] = { + 'column': column.column_name, + 'order': column.column_order, + 'ref_column': column.ref_column + } + order += 1 + return meta_data + + def get_filters(self, table_name): + _filter_map = {} + for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name). + filter(MetaDataColumn.show_filter == 1).all()): + _filter_map[column.column_name] = {'label': column.filter_label, 'widget': None} + print(f"retrieved {len(_filter_map)} filters: {_filter_map}") + return _filter_map + + def data(self, table, columns: dict, filters) -> list: + data = [] + entries = [] + if len(filters) == 0: + entries = self.session.query(table).all() + else: + entries = self.session.query(table).filter_by(**filters) + for entry in entries: + row = [] + for order in columns.keys(): + column_name = columns[order]['column'] + if str(column_name).endswith("_id"): + ref_table = column_name[:-3] + # print(f"{ref_table=}") + ref = getattr(entry, ref_table) + value = getattr(ref, "name") + # print(f"{value=}") + row.append(value) + else: + row.append(getattr(entry, column_name)) + # print(repr(row)) + data.append(row) + return data + + def get_data(self, table_name: str, columns: dict, where_clause: str) -> list: + data = [] + cursor = self.db_conn.cursor() + cursor.execute(self.get_statement(table_name, columns, where_clause)) + rows = cursor.fetchall() + print(len(rows)) + for row in rows: + # print(f"KontorDB.get_data: {row}") + data.append(list(row)) + cursor.close() + # print(f"KontorDB.getData: return {len(data)}") + if table_name == 'comic' and len(where_clause) == 0: + data.clear() + comics = self.session.query(Comic).all() + for item in comics: + # print(item) + row = [] + for order in columns.keys(): + column_name = columns[order]['column'] + if str(column_name).endswith("_id"): + ref_table = column_name[:-3] + # print(f"{ref_table=}") + ref = getattr(item, ref_table) + value = getattr(ref, "name") + # print(f"{value=}") + row.append(value) + else: + row.append(getattr(item, column_name)) + # print(repr(row)) + data.append(row) + return data + + def get_statement(self, table: str, header: dict, where_clause): + columns = "" + for index, column in header.items(): + if index > 0: + columns += ", " + columns += column['column'] + if len(columns) == 0: + columns = "*" + statement = f"SELECT {columns} FROM {table} {where_clause}" + print(f"{statement=}") + return statement + + def export_db(self, export_type: str, export_file_name: str, export_table_list: list): + print(f"export DB to {export_file_name} as {export_type}") + db = {} + for table in export_table_list: + columns = self.get_column_meta_data(table, view_only=False) + if table in self.registry: + model = self.registry[table] + else: + print(f"table {table} is not registered") + continue + rows = self.session.query(model).all() + entries = [] + print(f"found {len(rows)} entries") + print(f"found {len(columns)} columns") + for row in rows: + # print(row) + entry = {} + for order in columns: + # print(columns[order]) + column_name = columns[order]['column'] + # print(f"get value {column_name} from {row} of table {table}") + try: + value = getattr(row, column_name) + if isinstance(value, datetime): + entry[column_name] = str(value) + else: + entry[column_name] = value + except AttributeError as error: + print("could not get value") + entries.append(entry) + db[table] = entries + export_file = Path(export_file_name) + match export_type: + case "JSON": + json_dump = json.dumps(db, indent=4) + with open(export_file_name, "w") as dump_file: + dump_file.write(json_dump) + case "YAML": + export_file = Path(export_file_name) + case "SQLite": + export_file = Path(export_file_name) + case _: + print("unknown export type") + if export_file.exists(): + print(f"{export_file} exists") diff --git a/python/qt/database/base.py b/python/qt/database/base.py new file mode 100644 index 0000000..97d2990 --- /dev/null +++ b/python/qt/database/base.py @@ -0,0 +1,18 @@ +from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker, declarative_base + + +class Base(DeclarativeBase): + pass + +# class BaseModel: +# +# @classmethod +# def model_lookup_by_table_name(cls, table_name): +# registry_instance = getattr(cls, "registry") +# for mapper_ in registry_instance.mappers: +# model = mapper_.class_ +# model_class_name = model.__tablename__ +# if model_class_name == table_name: +# return model + +# Base = declarative_base(cls=BaseModel) \ No newline at end of file diff --git a/python/qt/database/comic.py b/python/qt/database/comic.py new file mode 100644 index 0000000..49d222f --- /dev/null +++ b/python/qt/database/comic.py @@ -0,0 +1,130 @@ +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from .base import Base + + +class Publisher(Base): + __tablename__ = "publisher" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), unique=True) + comics = relationship("Comic") + + def __repr__(self): + return f'Publisher({self.id} {self.name})' + + def __str__(self): + return self.__repr__() + + +class Comic(Base): + __tablename__ = 'comic' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + title = Column(String(length=255), unique=True) + publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False) + publisher = relationship("Publisher", back_populates="comics") + current_order = Column(BIT(1)) + completed = Column(BIT(1)) + issues = relationship("Issue") + story_arcs = relationship("StoryArc") + trade_paperbacks = relationship("TradePaperback") + volumes = relationship("Volume") + comic_works = relationship("ComicWork") + + def __repr__(self): + return f'Comic({self.id} {self.version} {self.title} {self.publisher.name})' + + def __str__(self): + return f'{self.title}({self.id})' + + +class Volume(Base): + __tablename__ = "volume" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="volumes") + issues = relationship("Issue") + + +class TradePaperback(Base): + __tablename__ = "trade_paperback" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False) + issue_start = Column(Integer) + issue_end = Column(Integer) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="trade_paperbacks") + + +class StoryArc(Base): + __tablename__ = "story_arc" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="story_arcs") + + +class Issue(Base): + __tablename__ = "issue" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + issue_number = Column(String(255)) + in_stock = Column(BIT(1)) + is_read = Column(BIT(1)) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="issues") + volume_id = Column(String, ForeignKey("volume.id"), nullable=True) + volume = relationship("Volume", back_populates="issues") + + +class Artist(Base): + __tablename__ = "artist" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False) + comic_works = relationship("ComicWork") + + +class WorkType(Base): + __tablename__ = "worktype" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False, unique=True) + comic_works = relationship("ComicWork") + + +class ComicWork(Base): + __tablename__ = "comic_work" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="comic_works") + artist_id = Column(String, ForeignKey("artist.id"), nullable=False) + artist = relationship("Artist", back_populates="comic_works") + work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False) + work_type = relationship("WorkType", back_populates="comic_works") diff --git a/qt/database/media.py b/python/qt/database/media.py similarity index 100% rename from qt/database/media.py rename to python/qt/database/media.py diff --git a/qt/database/metadata.py b/python/qt/database/metadata.py similarity index 100% rename from qt/database/metadata.py rename to python/qt/database/metadata.py diff --git a/python/qt/database/tysc.py b/python/qt/database/tysc.py new file mode 100644 index 0000000..733d7ed --- /dev/null +++ b/python/qt/database/tysc.py @@ -0,0 +1,131 @@ +from sqlalchemy import Column, DateTime, Integer, String, ForeignKey, UniqueConstraint +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from .base import Base + + +class Sport(Base): + __tablename__ = "sport" + __table_args__ = ( + UniqueConstraint("name"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255), nullable=False, index=True, unique=True) + teams = relationship("Team") + positions = relationship("FieldPosition") + + +class Team(Base): + __tablename__ = "team" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255), nullable=False, index=True, unique=True) + short_name = Column(String(255), nullable=False, ) + sport_id = Column(String, ForeignKey("sport.id"), nullable=False) + sport = relationship("Sport", back_populates="teams") + roosters = relationship("Rooster") + + +class FieldPosition(Base): + __tablename__ = "field_position" + __table_args__ = ( + UniqueConstraint("name", "sport_id"), + UniqueConstraint("short_name", "sport_id"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255), nullable=False, index=True) + short_name = Column(String(255), nullable=False) + sport_id = Column(String, ForeignKey("sport.id"), nullable=False, index=True) + sport = relationship("Sport", back_populates="positions") + roosters = relationship("Rooster") + + +class Player(Base): + __tablename__ = "player" + __table_args__ = ( + UniqueConstraint("first_name", "last_name"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + first_name = Column(String(255), nullable=False, index=True) + last_name = Column(String(255), nullable=False, index=True) + roosters = relationship("Rooster") + + def get_full_name(self) -> str: + return f"{self.last_name}, {self.first_name}" + + +class Rooster(Base): + __tablename__ = "rooster" + __table_args__ = ( + UniqueConstraint("year", "team_id", "player_id", "position_id"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + year = Column(Integer) + team_id = Column(String, ForeignKey("team.id"), nullable=False, index=True) + team = relationship("Team", back_populates="roosters") + player_id = Column(String, ForeignKey("player.id"), nullable=False, index=True) + player = relationship("Player", back_populates="roosters") + position_id = Column(String, ForeignKey("field_position.id"), nullable=False, index=True) + position = relationship("FieldPosition", back_populates="roosters") + cards = relationship("Card") + +class Vendor(Base): + __tablename__ = "vendor" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255), nullable=False, unique=True, index=True) + card_sets = relationship("CardSet") + cards = relationship("Card") + + +class CardSet(Base): + __tablename__ = "card_set" + __table_args__ = ( + UniqueConstraint("name", "vendor_id"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255), index=True) + parallel_set = Column(BIT(1)) + insert_set = Column(BIT(1)) + vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True) + vendor = relationship("Vendor", back_populates="card_sets") + cards = relationship("Card") + + +class Card(Base): + __tablename__ = "card" + __table_args__ = ( + UniqueConstraint("card_number", "year", "vendor_id", "card_set_id"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + card_number = Column(Integer, index=True) + year = Column(Integer, index=True) + card_set_id = Column(String, ForeignKey("card_set.id"), nullable=False) + card_set = relationship("CardSet", back_populates="cards") + rooster_id = Column(String, ForeignKey("rooster.id"), nullable=False) + rooster = relationship("Rooster", back_populates="cards") + vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False) + vendor = relationship("Vendor", back_populates="cards") diff --git a/python/qt/gui/__init__.py b/python/qt/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qt/gui/data_view.py b/python/qt/gui/data_view.py similarity index 100% rename from qt/gui/data_view.py rename to python/qt/gui/data_view.py diff --git a/qt/gui/data_view_model.py b/python/qt/gui/data_view_model.py similarity index 100% rename from qt/gui/data_view_model.py rename to python/qt/gui/data_view_model.py diff --git a/qt/gui/dialogs.py b/python/qt/gui/dialogs.py similarity index 100% rename from qt/gui/dialogs.py rename to python/qt/gui/dialogs.py diff --git a/qt/gui/main_window.py b/python/qt/gui/main_window.py similarity index 100% rename from qt/gui/main_window.py rename to python/qt/gui/main_window.py diff --git a/qt/gui/model_config.py b/python/qt/gui/model_config.py similarity index 100% rename from qt/gui/model_config.py rename to python/qt/gui/model_config.py diff --git a/qt/gui/table_model.py b/python/qt/gui/table_model.py similarity index 100% rename from qt/gui/table_model.py rename to python/qt/gui/table_model.py diff --git a/qt/kontor.py b/python/qt/kontor.py similarity index 100% rename from qt/kontor.py rename to python/qt/kontor.py diff --git a/qt/pysidedeploy.spec b/python/qt/pysidedeploy.spec similarity index 100% rename from qt/pysidedeploy.spec rename to python/qt/pysidedeploy.spec diff --git a/qt/res/application-export.png b/python/qt/res/application-export.png similarity index 100% rename from qt/res/application-export.png rename to python/qt/res/application-export.png diff --git a/qt/res/application-import.png b/python/qt/res/application-import.png similarity index 100% rename from qt/res/application-import.png rename to python/qt/res/application-import.png diff --git a/qt/res/arrow-circle-double.png b/python/qt/res/arrow-circle-double.png similarity index 100% rename from qt/res/arrow-circle-double.png rename to python/qt/res/arrow-circle-double.png diff --git a/qt/res/cross.png b/python/qt/res/cross.png similarity index 100% rename from qt/res/cross.png rename to python/qt/res/cross.png diff --git a/qt/res/tick.png b/python/qt/res/tick.png similarity index 100% rename from qt/res/tick.png rename to python/qt/res/tick.png From 453ad2af7bdbedf447eb5dd9b8a6aab85d46e4b2 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Tue, 14 Jan 2025 13:10:24 +0100 Subject: [PATCH 45/91] merged command line and gui app in one command --- python/{cli => }/.gitignore | 3 + python/{cli => }/CHANGELOG.md | 0 python/{cli => }/Dockerfile | 0 python/{cli => }/LICENSE.md | 0 python/{cli => }/MANIFEST.in | 0 python/{cli => }/Makefile | 0 python/{cli => }/README.md | 0 python/cli/kontor/controllers/clibase.py | 60 -------- python/cli/kontor/database/base.py | 20 --- python/{cli => }/config/kontor.yml.example | 0 python/{cli => }/docs/.gitkeep | 0 python/{cli => }/kontor/__init__.py | 0 .../{cli => }/kontor/controllers/__init__.py | 0 python/kontor/controllers/clibase.py | 42 ++++++ python/kontor/controllers/database.py | 32 ++++ python/{cli => }/kontor/core/__init__.py | 0 python/{cli => }/kontor/core/exc.py | 0 python/{cli => }/kontor/core/version.py | 0 python/{cli => }/kontor/database/__init__.py | 123 ++++++---------- python/kontor/database/base.py | 5 + python/{cli => }/kontor/database/comic.py | 0 python/{cli => }/kontor/database/media.py | 0 python/{cli => }/kontor/database/metadata.py | 1 + python/{qt => kontor}/database/tysc.py | 0 python/{cli => }/kontor/ext/__init__.py | 0 .../kontor/plugins => kontor/gui}/__init__.py | 0 python/{qt => kontor}/gui/data_view.py | 0 python/{qt => kontor}/gui/data_view_model.py | 0 python/{qt => kontor}/gui/dialogs.py | 0 python/kontor/gui/main_window.py | 139 ++++++++++++++++++ python/kontor/gui/model_config.py | 50 +++++++ python/kontor/gui/table_model.py | 108 ++++++++++++++ python/{cli => }/kontor/main.py | 14 +- .../templates => kontor/plugins}/__init__.py | 0 .../{qt => kontor}/res/application-export.png | Bin .../{qt => kontor}/res/application-import.png | Bin .../res/arrow-circle-double.png | Bin python/{qt => kontor}/res/cross.png | Bin python/{qt => kontor}/res/tick.png | Bin .../{qt/gui => kontor/templates}/__init__.py | 0 .../kontor/templates/command1.jinja2 | 0 python/main.py | 16 -- python/{cli => }/requirements-dev.txt | 0 python/{cli => }/requirements.txt | 1 + python/{cli => }/setup.cfg | 0 python/{cli => }/setup.py | 0 python/{cli => }/tests/conftest.py | 0 python/{cli => }/tests/test_kontor.py | 0 {python/qt => qt}/.gitignore | 0 {python/qt => qt}/database/__init__.py | 0 {python/qt => qt}/database/base.py | 0 {python/qt => qt}/database/comic.py | 0 {python/qt => qt}/database/media.py | 0 {python/qt => qt}/database/metadata.py | 0 {python/cli/kontor => qt}/database/tysc.py | 8 +- qt/gui/__init__.py | 0 qt/gui/data_view.py | 12 ++ qt/gui/data_view_model.py | 32 ++++ qt/gui/dialogs.py | 106 +++++++++++++ {python/qt => qt}/gui/main_window.py | 0 {python/qt => qt}/gui/model_config.py | 0 {python/qt => qt}/gui/table_model.py | 0 {python/qt => qt}/kontor.py | 0 {python/qt => qt}/pysidedeploy.spec | 0 qt/res/application-export.png | Bin 0 -> 513 bytes qt/res/application-import.png | Bin 0 -> 524 bytes qt/res/arrow-circle-double.png | Bin 0 -> 836 bytes qt/res/cross.png | Bin 0 -> 544 bytes qt/res/tick.png | Bin 0 -> 634 bytes 69 files changed, 589 insertions(+), 183 deletions(-) rename python/{cli => }/.gitignore (98%) rename python/{cli => }/CHANGELOG.md (100%) rename python/{cli => }/Dockerfile (100%) rename python/{cli => }/LICENSE.md (100%) rename python/{cli => }/MANIFEST.in (100%) rename python/{cli => }/Makefile (100%) rename python/{cli => }/README.md (100%) delete mode 100644 python/cli/kontor/controllers/clibase.py delete mode 100644 python/cli/kontor/database/base.py rename python/{cli => }/config/kontor.yml.example (100%) rename python/{cli => }/docs/.gitkeep (100%) rename python/{cli => }/kontor/__init__.py (100%) rename python/{cli => }/kontor/controllers/__init__.py (100%) create mode 100644 python/kontor/controllers/clibase.py create mode 100644 python/kontor/controllers/database.py rename python/{cli => }/kontor/core/__init__.py (100%) rename python/{cli => }/kontor/core/exc.py (100%) rename python/{cli => }/kontor/core/version.py (100%) rename python/{cli => }/kontor/database/__init__.py (55%) create mode 100644 python/kontor/database/base.py rename python/{cli => }/kontor/database/comic.py (100%) rename python/{cli => }/kontor/database/media.py (100%) rename python/{cli => }/kontor/database/metadata.py (99%) rename python/{qt => kontor}/database/tysc.py (100%) rename python/{cli => }/kontor/ext/__init__.py (100%) rename python/{cli/kontor/plugins => kontor/gui}/__init__.py (100%) rename python/{qt => kontor}/gui/data_view.py (100%) rename python/{qt => kontor}/gui/data_view_model.py (100%) rename python/{qt => kontor}/gui/dialogs.py (100%) create mode 100644 python/kontor/gui/main_window.py create mode 100644 python/kontor/gui/model_config.py create mode 100644 python/kontor/gui/table_model.py rename python/{cli => }/kontor/main.py (91%) rename python/{cli/kontor/templates => kontor/plugins}/__init__.py (100%) rename python/{qt => kontor}/res/application-export.png (100%) rename python/{qt => kontor}/res/application-import.png (100%) rename python/{qt => kontor}/res/arrow-circle-double.png (100%) rename python/{qt => kontor}/res/cross.png (100%) rename python/{qt => kontor}/res/tick.png (100%) rename python/{qt/gui => kontor/templates}/__init__.py (100%) rename python/{cli => }/kontor/templates/command1.jinja2 (100%) delete mode 100644 python/main.py rename python/{cli => }/requirements-dev.txt (100%) rename python/{cli => }/requirements.txt (90%) rename python/{cli => }/setup.cfg (100%) rename python/{cli => }/setup.py (100%) rename python/{cli => }/tests/conftest.py (100%) rename python/{cli => }/tests/test_kontor.py (100%) rename {python/qt => qt}/.gitignore (100%) rename {python/qt => qt}/database/__init__.py (100%) rename {python/qt => qt}/database/base.py (100%) rename {python/qt => qt}/database/comic.py (100%) rename {python/qt => qt}/database/media.py (100%) rename {python/qt => qt}/database/metadata.py (100%) rename {python/cli/kontor => qt}/database/tysc.py (94%) create mode 100644 qt/gui/__init__.py create mode 100644 qt/gui/data_view.py create mode 100644 qt/gui/data_view_model.py create mode 100644 qt/gui/dialogs.py rename {python/qt => qt}/gui/main_window.py (100%) rename {python/qt => qt}/gui/model_config.py (100%) rename {python/qt => qt}/gui/table_model.py (100%) rename {python/qt => qt}/kontor.py (100%) rename {python/qt => qt}/pysidedeploy.spec (100%) mode change 100755 => 100644 create mode 100644 qt/res/application-export.png create mode 100644 qt/res/application-import.png create mode 100644 qt/res/arrow-circle-double.png create mode 100644 qt/res/cross.png create mode 100644 qt/res/tick.png diff --git a/python/cli/.gitignore b/python/.gitignore similarity index 98% rename from python/cli/.gitignore rename to python/.gitignore index a74b246..0f47561 100644 --- a/python/cli/.gitignore +++ b/python/.gitignore @@ -95,6 +95,9 @@ venv.bak/ .spyderproject .spyproject +# PyCharm +.idea/ + # Rope project settings .ropeproject diff --git a/python/cli/CHANGELOG.md b/python/CHANGELOG.md similarity index 100% rename from python/cli/CHANGELOG.md rename to python/CHANGELOG.md diff --git a/python/cli/Dockerfile b/python/Dockerfile similarity index 100% rename from python/cli/Dockerfile rename to python/Dockerfile diff --git a/python/cli/LICENSE.md b/python/LICENSE.md similarity index 100% rename from python/cli/LICENSE.md rename to python/LICENSE.md diff --git a/python/cli/MANIFEST.in b/python/MANIFEST.in similarity index 100% rename from python/cli/MANIFEST.in rename to python/MANIFEST.in diff --git a/python/cli/Makefile b/python/Makefile similarity index 100% rename from python/cli/Makefile rename to python/Makefile diff --git a/python/cli/README.md b/python/README.md similarity index 100% rename from python/cli/README.md rename to python/README.md diff --git a/python/cli/kontor/controllers/clibase.py b/python/cli/kontor/controllers/clibase.py deleted file mode 100644 index 8d69fb2..0000000 --- a/python/cli/kontor/controllers/clibase.py +++ /dev/null @@ -1,60 +0,0 @@ - -from cement import Controller, ex -from cement.utils.version import get_version_banner -from ..core.version import get_version - -VERSION_BANNER = """ -Kontor CLI Tool %s -%s -""" % (get_version(), get_version_banner()) - - -class CliBase(Controller): - class Meta: - label = 'base' - - # text displayed at the top of --help output - description = 'Kontor CLI Tool' - - # text displayed at the bottom of --help output - epilog = 'Usage: kontor command1 --foo bar' - - # controller level arguments. ex: 'kontor --version' - arguments = [ - ### add a version banner - ( [ '-v', '--version' ], - { 'action' : 'version', - 'version' : VERSION_BANNER } ), - ] - - - def _default(self): - """Default action if no sub-command is passed.""" - - self.app.args.print_help() - - - @ex( - help='example sub command1', - - # sub-command level arguments. ex: 'kontor command1 --foo bar' - arguments=[ - ### add a sample foo option under subcommand namespace - ( [ '-f', '--foo' ], - { 'help' : 'notorious foo option', - 'action' : 'store', - 'dest' : 'foo' } ), - ], - ) - def command1(self): - """Example sub-command.""" - - data = { - 'foo' : 'bar', - } - - ### do something with arguments - if self.app.pargs.foo is not None: - data['foo'] = self.app.pargs.foo - - self.app.render(data, 'command1.jinja2') diff --git a/python/cli/kontor/database/base.py b/python/cli/kontor/database/base.py deleted file mode 100644 index f97d724..0000000 --- a/python/cli/kontor/database/base.py +++ /dev/null @@ -1,20 +0,0 @@ -from sqlalchemy import Column, String, DateTime, Integer -from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker, declarative_base - - -# class Base(DeclarativeBase): -# pass - -class BaseModel: - - @classmethod - def model_lookup_by_table_name(cls, table_name): - registry_instance = getattr(cls, "registry") - for mapper_ in registry_instance.mappers: - model = mapper_.class_ - model_class_name = model.__tablename__ - if model_class_name == table_name: - return model - - -Base = declarative_base(cls=BaseModel) \ No newline at end of file diff --git a/python/cli/config/kontor.yml.example b/python/config/kontor.yml.example similarity index 100% rename from python/cli/config/kontor.yml.example rename to python/config/kontor.yml.example diff --git a/python/cli/docs/.gitkeep b/python/docs/.gitkeep similarity index 100% rename from python/cli/docs/.gitkeep rename to python/docs/.gitkeep diff --git a/python/cli/kontor/__init__.py b/python/kontor/__init__.py similarity index 100% rename from python/cli/kontor/__init__.py rename to python/kontor/__init__.py diff --git a/python/cli/kontor/controllers/__init__.py b/python/kontor/controllers/__init__.py similarity index 100% rename from python/cli/kontor/controllers/__init__.py rename to python/kontor/controllers/__init__.py diff --git a/python/kontor/controllers/clibase.py b/python/kontor/controllers/clibase.py new file mode 100644 index 0000000..568a546 --- /dev/null +++ b/python/kontor/controllers/clibase.py @@ -0,0 +1,42 @@ +from PySide6.QtWidgets import QApplication +from cement import Controller, ex +from cement.utils.version import get_version_banner +from ..core.version import get_version +from ..gui.main_window import MainWindow + +VERSION_BANNER = """ +Kontor CLI Tool %s +%s +""" % (get_version(), get_version_banner()) + + +class CliBase(Controller): + class Meta: + label = 'base' + + # text displayed at the top of --help output + description = 'Kontor CLI Tool' + + # text displayed at the bottom of --help output + epilog = 'Usage: kontor gui|database' + + # controller level arguments. ex: 'kontor --version' + arguments = [ + ### add a version banner + (['-v', '--version'], + {'action': 'version', + 'version': VERSION_BANNER}), + ] + + def _default(self): + """Default action if no sub-command is passed.""" + self.gui() + + @ex( + help='start GUI' + ) + def gui(self): + application = QApplication([]) + window = MainWindow(self.app.session, self.app.log) + window.show() + application.exec() diff --git a/python/kontor/controllers/database.py b/python/kontor/controllers/database.py new file mode 100644 index 0000000..246d4f8 --- /dev/null +++ b/python/kontor/controllers/database.py @@ -0,0 +1,32 @@ +from cement import Controller, ex + +from ..database import KontorDB + + +class Database(Controller): + + class Meta: + label = 'database' + stacked_type = 'nested' + stacked_on = 'base' + + @ex( + help='export database to given file', + arguments=[ + (['-f', '--file'], + {'help': 'file to store database content', + 'action': 'store', + 'dest': 'db_file'}) + ], + ) + def export(self): + data = { + 'db_file': 'data.json', + 'export_type': 'JSON', + } + if self.app.pargs.db_file is not None: + data['db_file'] = self.app.pargs.db_file + kontor_db = KontorDB(self.app.session, self.app.log) + table_list = kontor_db.get_table_names() + kontor_db.export_db(data['export_type'], data['db_file'], table_list) + self.app.render(data, 'command1.jinja2') diff --git a/python/cli/kontor/core/__init__.py b/python/kontor/core/__init__.py similarity index 100% rename from python/cli/kontor/core/__init__.py rename to python/kontor/core/__init__.py diff --git a/python/cli/kontor/core/exc.py b/python/kontor/core/exc.py similarity index 100% rename from python/cli/kontor/core/exc.py rename to python/kontor/core/exc.py diff --git a/python/cli/kontor/core/version.py b/python/kontor/core/version.py similarity index 100% rename from python/cli/kontor/core/version.py rename to python/kontor/core/version.py diff --git a/python/cli/kontor/database/__init__.py b/python/kontor/database/__init__.py similarity index 55% rename from python/cli/kontor/database/__init__.py rename to python/kontor/database/__init__.py index 95ce1d8..c7e6cb5 100644 --- a/python/cli/kontor/database/__init__.py +++ b/python/kontor/database/__init__.py @@ -2,37 +2,40 @@ import json from datetime import datetime from pathlib import Path -import mariadb -from sqlalchemy import create_engine, select, text, MetaData, join -from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker - from .base import Base -from .comic import Comic +from .comic import Comic, Artist, Publisher, Issue, StoryArc, TradePaperback, Volume, ComicWork, WorkType from .metadata import MetaDataTable, MetaDataColumn +from .tysc import Card, CardSet, Sport, Team, FieldPosition, Rooster, Player, Vendor +from .media import MediaFile class KontorDB: - def __init__(self, db_config): - self.db_conn = mariadb.connect( - host=db_config['mariadb']['host'], - port=db_config['mariadb']['port'], - user=db_config['mariadb']['user'], - password=db_config['mariadb']['password'], - database=db_config['mariadb']['database'] - ) - connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}' - .format( - db_config['mariadb']['user'], - db_config['mariadb']['password'], - db_config['mariadb']['host'], - db_config['mariadb']['port'], - db_config['mariadb']['database'])) - # engine = create_engine(connect_string, echo=True) - engine = create_engine(connect_string) - Base.metadata.create_all(bind=engine) - __session__ = sessionmaker(bind=engine) - self.session = __session__() + def __init__(self, db_session, log): + self.session = db_session + self.log = log + self.registry = {} + self.init_registry() + + def init_registry(self): + self.registry['card'] = Card + self.registry['card_set'] = CardSet + self.registry['sport'] = Sport + self.registry['team'] = Team + self.registry['field_position'] = FieldPosition + self.registry['rooster'] = Rooster + self.registry['player'] = Player + self.registry['vendor'] = Vendor + self.registry['artist'] = Artist + self.registry['publisher'] = Publisher + self.registry['comic'] = Comic + self.registry['issue'] = Issue + self.registry['story_arc'] = StoryArc + self.registry['trade_paperback'] = TradePaperback + self.registry['volume'] = Volume + self.registry['comic_work'] = ComicWork + self.registry['worktype'] = WorkType + self.registry['media_file'] = MediaFile def get_table_names(self) -> list: tables = self.session.query(MetaDataTable).all() @@ -69,7 +72,7 @@ class KontorDB: filter(MetaDataTable.table_name == table_name). filter(MetaDataColumn.show_filter == 1).all()): _filter_map[column.column_name] = {'label': column.filter_label, 'widget': None} - print(f"retrieved {len(_filter_map)} filters: {_filter_map}") + self.log.info(f"retrieved {len(_filter_map)} filters: {_filter_map}") return _filter_map def data(self, table, columns: dict, filters) -> list: @@ -96,67 +99,27 @@ class KontorDB: data.append(row) return data - def get_data(self, table_name: str, columns: dict, where_clause: str) -> list: - data = [] - cursor = self.db_conn.cursor() - cursor.execute(self.get_statement(table_name, columns, where_clause)) - rows = cursor.fetchall() - print(len(rows)) - for row in rows: - # print(f"KontorDB.get_data: {row}") - data.append(list(row)) - cursor.close() - # print(f"KontorDB.getData: return {len(data)}") - if table_name == 'comic' and len(where_clause) == 0: - data.clear() - comics = self.session.query(Comic).all() - for item in comics: - # print(item) - row = [] - for order in columns.keys(): - column_name = columns[order]['column'] - if str(column_name).endswith("_id"): - ref_table = column_name[:-3] - # print(f"{ref_table=}") - ref = getattr(item, ref_table) - value = getattr(ref, "name") - # print(f"{value=}") - row.append(value) - else: - row.append(getattr(item, column_name)) - # print(repr(row)) - data.append(row) - return data - - def get_statement(self, table: str, header: dict, where_clause): - columns = "" - for index, column in header.items(): - if index > 0: - columns += ", " - columns += column['column'] - if len(columns) == 0: - columns = "*" - statement = f"SELECT {columns} FROM {table} {where_clause}" - print(f"{statement=}") - return statement - def export_db(self, export_type: str, export_file_name: str, export_table_list: list): - print(f"export DB to {export_file_name} as {export_type}") + self.log.info(f"export DB to {export_file_name} as {export_type}") db = {} for table in export_table_list: columns = self.get_column_meta_data(table, view_only=False) - model = Base.model_lookup_by_table_name(table) + if table in self.registry: + model = self.registry[table] + else: + print(f"table {table} is not registered") + continue rows = self.session.query(model).all() entries = [] - print(f"found {len(rows)} entries") - print(f"found {len(columns)} columns") + self.log.debug(f"found {len(rows)} entries") + self.log.debug(f"found {len(columns)} columns") for row in rows: - print(row) + # print(row) entry = {} for order in columns: - print(columns[order]) + # print(columns[order]) column_name = columns[order]['column'] - print(f"get value {column_name} from {row} of table {table}") + # print(f"get value {column_name} from {row} of table {table}") try: value = getattr(row, column_name) if isinstance(value, datetime): @@ -164,7 +127,7 @@ class KontorDB: else: entry[column_name] = value except AttributeError as error: - print("could not get value") + self.log.debug("could not get value") entries.append(entry) db[table] = entries export_file = Path(export_file_name) @@ -178,6 +141,6 @@ class KontorDB: case "SQLite": export_file = Path(export_file_name) case _: - print("unknown export type") + self.log.debug("unknown export type") if export_file.exists(): - print(f"{export_file} exists") + self.log.debug(f"{export_file} exists") diff --git a/python/kontor/database/base.py b/python/kontor/database/base.py new file mode 100644 index 0000000..fa2b68a --- /dev/null +++ b/python/kontor/database/base.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import DeclarativeBase + + +class Base(DeclarativeBase): + pass diff --git a/python/cli/kontor/database/comic.py b/python/kontor/database/comic.py similarity index 100% rename from python/cli/kontor/database/comic.py rename to python/kontor/database/comic.py diff --git a/python/cli/kontor/database/media.py b/python/kontor/database/media.py similarity index 100% rename from python/cli/kontor/database/media.py rename to python/kontor/database/media.py diff --git a/python/cli/kontor/database/metadata.py b/python/kontor/database/metadata.py similarity index 99% rename from python/cli/kontor/database/metadata.py rename to python/kontor/database/metadata.py index 2975e10..d7dd4c0 100644 --- a/python/cli/kontor/database/metadata.py +++ b/python/kontor/database/metadata.py @@ -20,6 +20,7 @@ class MetaDataTable(Base): def __str__(self): return f'{self.table_name}({self.id})' + class MetaDataColumn(Base): __tablename__ = 'meta_data_column' id = Column(String, primary_key=True) diff --git a/python/qt/database/tysc.py b/python/kontor/database/tysc.py similarity index 100% rename from python/qt/database/tysc.py rename to python/kontor/database/tysc.py diff --git a/python/cli/kontor/ext/__init__.py b/python/kontor/ext/__init__.py similarity index 100% rename from python/cli/kontor/ext/__init__.py rename to python/kontor/ext/__init__.py diff --git a/python/cli/kontor/plugins/__init__.py b/python/kontor/gui/__init__.py similarity index 100% rename from python/cli/kontor/plugins/__init__.py rename to python/kontor/gui/__init__.py diff --git a/python/qt/gui/data_view.py b/python/kontor/gui/data_view.py similarity index 100% rename from python/qt/gui/data_view.py rename to python/kontor/gui/data_view.py diff --git a/python/qt/gui/data_view_model.py b/python/kontor/gui/data_view_model.py similarity index 100% rename from python/qt/gui/data_view_model.py rename to python/kontor/gui/data_view_model.py diff --git a/python/qt/gui/dialogs.py b/python/kontor/gui/dialogs.py similarity index 100% rename from python/qt/gui/dialogs.py rename to python/kontor/gui/dialogs.py diff --git a/python/kontor/gui/main_window.py b/python/kontor/gui/main_window.py new file mode 100644 index 0000000..24a84a6 --- /dev/null +++ b/python/kontor/gui/main_window.py @@ -0,0 +1,139 @@ +from PySide6.QtGui import QAction, QIcon +from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView +from PySide6.QtWidgets import QLabel, QMainWindow + +from ..database import KontorDB +from ..database.media import MediaFile +from ..database.comic import Comic +from .dialogs import ExportKontorDialog, ImportKontorDialog +from .model_config import KontorModelConfig +from .table_model import KontorTableModel + + +class MainWindow(QMainWindow): + + def __init__(self, session, log): + super().__init__() + + self.tick = QIcon('kontor/res/tick.png') + self.cross = QIcon('kontor/res/cross.png') + self.import_icon = QIcon("kontor/res/application-import.png") + self.export_icon = QIcon("kontor/res/application-export.png") + self.circle_icon = QIcon("kontor/res/arrow-circle-double.png") + + self.setWindowTitle("Kontor") + self.setMinimumSize(800, 500) + self._create_actions() + self._create_menubar() + self._create_toolbars() + self._create_statusbar() + + self.data = [] + self.filter = {} + self.kontor_db = KontorDB(session, log) + self.log = log + self.central_widget = QWidget() + parent_layout = QVBoxLayout() + self.central_widget.setLayout(parent_layout) + self.tabs = QTabWidget() + self.tabs.addTab(self.generate_data_tab("comic", Comic), "Comics") + self.tabs.addTab(self.generate_data_tab("media_file", MediaFile), "MediaFile") + self.tabs.currentChanged.connect(self._tab_changed) + #label.setAlignment(Qt.AlignmentFlag.AlignCenter) + parent_layout.addWidget(self.tabs) + + self.setCentralWidget(self.central_widget) + + def _create_actions(self): + self.newAction = QAction("&New", self) + self.aboutAction = QAction("&Über...", self) + self.aboutAction.triggered.connect(self.about) + self.importAction = QAction(self.import_icon, "&Import", self) + self.importAction.triggered.connect(self.import_from_file) + self.exportAction = QAction(self.export_icon, "&Export", self) + self.exportAction.triggered.connect(self.export_to_file) + self.refreshAction = QAction(self.circle_icon, "&Refresh", self) + self.refreshAction.triggered.connect(self.refresh) + self.updateTitleAction = QAction("&Update Titles", self) + self.downloadAction = QAction("&Download Videos", self) + self.exitAction = QAction("&Beenden", self) + self.exitAction.setShortcut("Alt+F4") + self.exitAction.triggered.connect(self.close) + + def _create_menubar(self): + menu_bar = self.menuBar() + # File menu + file_menu = QMenu("&Datei") + menu_bar.addMenu(file_menu) + file_menu.addAction(self.exitAction) + # Kontor menu + kontor_menu = QMenu("&Kontor") + menu_bar.addMenu(kontor_menu) + kontor_menu.addAction(self.importAction) + kontor_menu.addAction(self.exportAction) + comic_menu = QMenu("&Comic") + tysc_menu = QMenu("&TradeYourSportCards") + media_file_menu = QMenu("&MediaFile") + media_file_menu.addAction(self.updateTitleAction) + media_file_menu.addAction(self.downloadAction) + kontor_menu.addMenu(comic_menu) + kontor_menu.addMenu(tysc_menu) + kontor_menu.addMenu(media_file_menu) + # Help menu + help_menu = QMenu("&Hilfe") + menu_bar.addMenu(help_menu) + help_menu.addAction(self.aboutAction) + + def _create_toolbars(self): + # Kontor toolbar + kontor_tool_bar = self.addToolBar("Kontor") + kontor_tool_bar.addAction(self.importAction) + kontor_tool_bar.addAction(self.exportAction) + kontor_tool_bar.addAction(self.refreshAction) + + def _create_statusbar(self): + self.statusBar = self.statusBar() + self.statusBar.showMessage("Kontor ready", 6000) + self.status_label = QLabel("") + self.statusBar.addPermanentWidget(self.status_label) + + def about(self): + QMessageBox.about(self.central_widget, "Über Kontor", f"Python: 3.11\nKontor: 0.1.0") + + def import_from_file(self): + import_dlg = ImportKontorDialog(self) + if import_dlg.exec(): + print(f"import DB from file {import_dlg.file_name}") + else: + print("no nothing for import") + pass + + def export_to_file(self): + export_dlg = ExportKontorDialog(self, self.kontor_db) + if export_dlg.exec(): + self.log.info(export_dlg.get_tables_to_export()) + self.log.info(f"export DB to {export_dlg.file_name}") + self.statusBar.showMessage(f"export DB to {export_dlg.file_name}", 3000) + self.kontor_db.export_db(export_dlg.current_export_type, export_dlg.file_name, export_dlg.get_tables_to_export()) + else: + self.statusBar.showMessage("Export cancelled", 3000) + + def refresh(self): + self.data[self.tabs.currentIndex()].refresh() + + def _tab_changed(self, tab_index): + self.data[tab_index].refresh() + + def generate_data_tab(self, table_name, table): + data_tab = QWidget() + table_config = KontorModelConfig(self.kontor_db, self, table_name, table) + model = KontorTableModel(table_config) + layout = QVBoxLayout() + self.data.append(model) + data_tab.setLayout(layout) + table_view = QTableView() + table_view.setModel(model) + layout.addLayout(table_config.get_filter_layout()) + layout.addWidget(table_view) + model.refresh() + return data_tab diff --git a/python/kontor/gui/model_config.py b/python/kontor/gui/model_config.py new file mode 100644 index 0000000..ce61107 --- /dev/null +++ b/python/kontor/gui/model_config.py @@ -0,0 +1,50 @@ +import mariadb +from PySide6.QtWidgets import QHBoxLayout, QCheckBox + +from ..database import KontorDB + + +class KontorModelConfig: + + def __init__(self, kontor_db: KontorDB, main_window, table_name: str, table): + self.header = {} + self.filter = {} + self.main_window = main_window + self._table_name = table_name + self._table = table + self.kontor_db = kontor_db + self.get_table_config() + + def get_table_config(self): + self.header = self.kontor_db.get_column_meta_data(self._table_name) + self.filter = self.kontor_db.get_filters(self._table_name) + + def filters(self) -> dict: + _filters = {} + # print(self.filter["download"].isChecked()) + for column, filter_info in self.filter.items(): + # print(column, filter_info) + if filter_info['widget'].isChecked(): + _filters[column] = True + # print(f"{filter_rule=}") + return _filters + + def get_data(self) -> list: + # data = self.kontor_db.get_data(self._table_name, self.header, self.get_filter()) + # data.clear() + data = self.kontor_db.data(self._table, self.header, self.filters()) + # print(f"KontorModelConfig.get_data: {len(data)}") + # comics = self.kontor_db.session.query(Comic).all() + # print(f'{len(comics)} Comics loaded') + return data + + def get_filter_layout(self) -> QHBoxLayout: + filter_layout = QHBoxLayout() + for column, filter_info in self.filter.items(): + filter_checkbox = QCheckBox() + filter_checkbox.setText(filter_info['label']) + filter_checkbox.checkStateChanged.connect(self.main_window.refresh) + self.filter[column]['widget'] = filter_checkbox + filter_layout.addWidget(filter_checkbox) + filter_layout.addStretch() + return filter_layout diff --git a/python/kontor/gui/table_model.py b/python/kontor/gui/table_model.py new file mode 100644 index 0000000..cd08ce1 --- /dev/null +++ b/python/kontor/gui/table_model.py @@ -0,0 +1,108 @@ +from datetime import datetime + +from PySide6.QtCore import QAbstractTableModel, QModelIndex +from PySide6.QtGui import Qt + +from .model_config import KontorModelConfig + + +class KontorTableModel(QAbstractTableModel): + + def __init__(self, model_config: KontorModelConfig): + super().__init__() + self._main_window = model_config.main_window + self._config = model_config + self._data = [] + + def refresh(self): + data = self._config.get_data() + count = 0 + # print(data) + if data is not None: + self.beginResetModel() + self._data.clear() + self._data = data + self.endResetModel() + count = len(data) + # print(data) + # print(self._data) + self.layoutChanged.emit() + self._main_window.statusBar.showMessage(f"{count} Einträge geladen", 3000) + + def rowCount(self, parent=QModelIndex()): + # The length of the outer list. + if self._data is None: + return 0 + return len(self._data) + + def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): + if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: + return self._config.header[col]['label'] + if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: + return str(col+1) + + def data(self, index, role=Qt.ItemDataRole.DisplayRole): + if self._data is None: + return None + value = self._data[index.row()][index.column()] + # print('{}:: {}:: {}: {}'.format(index, role, value, type(value))) + if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole: + if isinstance(value, datetime): + return value.strftime("%Y-%m-%d %M:%M:%S") + if isinstance(value, str): + return value + if isinstance(value, bytes): + if value == b'\x01': + return self._main_window.tick + else: + return self._main_window.cross + if isinstance(value, int): + # print('{}:: {}: {}'.format(index, value, type(value))) + if value == 1: + return self._main_window.tick + else: + return self._main_window.cross + if isinstance(value, bool): + if value: + return self._main_window.tick + else: + return self._main_window.cross + return str(value) + if role == Qt.ItemDataRole.DecorationRole: + if isinstance(value, bytes): + if value == b'\x01': + return self._main_window.tick + else: + return self._main_window.cross + if isinstance(value, int): + if value == 1: + return self._main_window.tick + else: + return self._main_window.cross + if isinstance(value, bool): + if value: + return self._main_window.tick + else: + return self._main_window.cross + + def columnCount(self, index=QModelIndex()): + # The following takes the first sub-list, and returns + # the length (only works if all rows are an equal length) + # print(f"Header count: {len(self._config.get_header())}") + return len(self._config.header) + + def setData(self, index, value, role: int) -> bool: + print(index, role) + if role == Qt.ItemDataRole.EditRole: + self._data[index.row()][index.column()] = value + print(self._data[index.row()][index.column()]) + self.dataChanged.emit(index, index) + return True + if role == Qt.ItemDataRole.CheckStateRole: + print("role == Qt.ItemDataRole.CheckStateRole") + checked = value == Qt.CheckState.Checked + self._data[index.row()][index.column()] = checked + return False + + def flags(self, index): + return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate diff --git a/python/cli/kontor/main.py b/python/kontor/main.py similarity index 91% rename from python/cli/kontor/main.py rename to python/kontor/main.py index 4c71aab..aa71e06 100644 --- a/python/cli/kontor/main.py +++ b/python/kontor/main.py @@ -1,4 +1,3 @@ - from cement import App, TestApp, init_defaults from cement.core.exc import CaughtSignal from sqlalchemy import create_engine @@ -7,6 +6,7 @@ from sqlalchemy.orm import sessionmaker from .core.exc import KontorError from .database.base import Base from .controllers.clibase import CliBase +from .controllers.database import Database # configuration defaults CONFIG = init_defaults('kontor', 'mariadb') @@ -17,6 +17,7 @@ CONFIG['mariadb']['host'] = '127.0.0.1' CONFIG['mariadb']['port'] = '3306' CONFIG['mariadb']['database'] = 'kontor' + def extend_sqlalchemy(app): app.log.info('extending kontor application with sqlalchemy') connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format( @@ -33,6 +34,11 @@ def extend_sqlalchemy(app): app.extend('session', __session__()) +def close_session(app): + app.log.info('close session') + app.session.close() + + class Kontor(App): """Kontor primary application.""" @@ -66,14 +72,16 @@ class Kontor(App): hooks = [ ('post_setup', extend_sqlalchemy), + ('pre_close', close_session), ] # register handlers handlers = [ - CliBase + CliBase, + Database, ] -class KontorTest(TestApp,Kontor): +class KontorTest(TestApp, Kontor): """A sub-class of Kontor that is better suited for testing.""" class Meta: diff --git a/python/cli/kontor/templates/__init__.py b/python/kontor/plugins/__init__.py similarity index 100% rename from python/cli/kontor/templates/__init__.py rename to python/kontor/plugins/__init__.py diff --git a/python/qt/res/application-export.png b/python/kontor/res/application-export.png similarity index 100% rename from python/qt/res/application-export.png rename to python/kontor/res/application-export.png diff --git a/python/qt/res/application-import.png b/python/kontor/res/application-import.png similarity index 100% rename from python/qt/res/application-import.png rename to python/kontor/res/application-import.png diff --git a/python/qt/res/arrow-circle-double.png b/python/kontor/res/arrow-circle-double.png similarity index 100% rename from python/qt/res/arrow-circle-double.png rename to python/kontor/res/arrow-circle-double.png diff --git a/python/qt/res/cross.png b/python/kontor/res/cross.png similarity index 100% rename from python/qt/res/cross.png rename to python/kontor/res/cross.png diff --git a/python/qt/res/tick.png b/python/kontor/res/tick.png similarity index 100% rename from python/qt/res/tick.png rename to python/kontor/res/tick.png diff --git a/python/qt/gui/__init__.py b/python/kontor/templates/__init__.py similarity index 100% rename from python/qt/gui/__init__.py rename to python/kontor/templates/__init__.py diff --git a/python/cli/kontor/templates/command1.jinja2 b/python/kontor/templates/command1.jinja2 similarity index 100% rename from python/cli/kontor/templates/command1.jinja2 rename to python/kontor/templates/command1.jinja2 diff --git a/python/main.py b/python/main.py deleted file mode 100644 index 76d0e8c..0000000 --- a/python/main.py +++ /dev/null @@ -1,16 +0,0 @@ -# This is a sample Python script. - -# Press Umschalt+F10 to execute it or replace it with your code. -# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings. - - -def print_hi(name): - # Use a breakpoint in the code line below to debug your script. - print(f'Hi, {name}') # Press Strg+F8 to toggle the breakpoint. - - -# Press the green button in the gutter to run the script. -if __name__ == '__main__': - print_hi('PyCharm') - -# See PyCharm help at https://www.jetbrains.com/help/pycharm/ diff --git a/python/cli/requirements-dev.txt b/python/requirements-dev.txt similarity index 100% rename from python/cli/requirements-dev.txt rename to python/requirements-dev.txt diff --git a/python/cli/requirements.txt b/python/requirements.txt similarity index 90% rename from python/cli/requirements.txt rename to python/requirements.txt index bee2df1..06a15f9 100644 --- a/python/cli/requirements.txt +++ b/python/requirements.txt @@ -4,3 +4,4 @@ cement[yaml] cement[colorlog] mariadb sqlalchemy +PySide6 diff --git a/python/cli/setup.cfg b/python/setup.cfg similarity index 100% rename from python/cli/setup.cfg rename to python/setup.cfg diff --git a/python/cli/setup.py b/python/setup.py similarity index 100% rename from python/cli/setup.py rename to python/setup.py diff --git a/python/cli/tests/conftest.py b/python/tests/conftest.py similarity index 100% rename from python/cli/tests/conftest.py rename to python/tests/conftest.py diff --git a/python/cli/tests/test_kontor.py b/python/tests/test_kontor.py similarity index 100% rename from python/cli/tests/test_kontor.py rename to python/tests/test_kontor.py diff --git a/python/qt/.gitignore b/qt/.gitignore similarity index 100% rename from python/qt/.gitignore rename to qt/.gitignore diff --git a/python/qt/database/__init__.py b/qt/database/__init__.py similarity index 100% rename from python/qt/database/__init__.py rename to qt/database/__init__.py diff --git a/python/qt/database/base.py b/qt/database/base.py similarity index 100% rename from python/qt/database/base.py rename to qt/database/base.py diff --git a/python/qt/database/comic.py b/qt/database/comic.py similarity index 100% rename from python/qt/database/comic.py rename to qt/database/comic.py diff --git a/python/qt/database/media.py b/qt/database/media.py similarity index 100% rename from python/qt/database/media.py rename to qt/database/media.py diff --git a/python/qt/database/metadata.py b/qt/database/metadata.py similarity index 100% rename from python/qt/database/metadata.py rename to qt/database/metadata.py diff --git a/python/cli/kontor/database/tysc.py b/qt/database/tysc.py similarity index 94% rename from python/cli/kontor/database/tysc.py rename to qt/database/tysc.py index 54cacea..733d7ed 100644 --- a/python/cli/kontor/database/tysc.py +++ b/qt/database/tysc.py @@ -28,7 +28,7 @@ class Team(Base): name = Column(String(255), nullable=False, index=True, unique=True) short_name = Column(String(255), nullable=False, ) sport_id = Column(String, ForeignKey("sport.id"), nullable=False) - sport = relationship("Sport", back_populates="positions") + sport = relationship("Sport", back_populates="teams") roosters = relationship("Rooster") @@ -81,7 +81,7 @@ class Rooster(Base): player_id = Column(String, ForeignKey("player.id"), nullable=False, index=True) player = relationship("Player", back_populates="roosters") position_id = Column(String, ForeignKey("field_position.id"), nullable=False, index=True) - position = relationship("roosters") + position = relationship("FieldPosition", back_populates="roosters") cards = relationship("Card") class Vendor(Base): @@ -124,8 +124,8 @@ class Card(Base): card_number = Column(Integer, index=True) year = Column(Integer, index=True) card_set_id = Column(String, ForeignKey("card_set.id"), nullable=False) - card_set = relationship("cards") + card_set = relationship("CardSet", back_populates="cards") rooster_id = Column(String, ForeignKey("rooster.id"), nullable=False) - rooster = relationship("cards") + rooster = relationship("Rooster", back_populates="cards") vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False) vendor = relationship("Vendor", back_populates="cards") diff --git a/qt/gui/__init__.py b/qt/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qt/gui/data_view.py b/qt/gui/data_view.py new file mode 100644 index 0000000..91c66d2 --- /dev/null +++ b/qt/gui/data_view.py @@ -0,0 +1,12 @@ +from abc import ABC, abstractmethod + + +class DataViewMeta(ABC): + @abstractmethod + def get_header(self): + pass + + +class ComicView(DataViewMeta): + def get_header(self): + pass diff --git a/qt/gui/data_view_model.py b/qt/gui/data_view_model.py new file mode 100644 index 0000000..4ffdc8c --- /dev/null +++ b/qt/gui/data_view_model.py @@ -0,0 +1,32 @@ +from typing import List + +from PySide6.QtCore import QModelIndex, QAbstractTableModel +from PySide6.QtGui import Qt + +from gui.data_view import DataViewMeta + + +class DataViewModel(QAbstractTableModel): + def __init__(self): + super().__init__() + self.main_window = None + self._config = None + self._data = List[DataViewMeta] + + def rowCount(self, parent=QModelIndex()): + return len(self._data) + + def columnCount(self, parent=QModelIndex()): + return 0 + + def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole): + return None + + def data(self, index, role=Qt.ItemDataRole.DisplayRole): + return None + + def setData(self, index, value, role=Qt.ItemDataRole.EditRole): + return False + + def flags(self, index): + return None diff --git a/qt/gui/dialogs.py b/qt/gui/dialogs.py new file mode 100644 index 0000000..21dbe92 --- /dev/null +++ b/qt/gui/dialogs.py @@ -0,0 +1,106 @@ +from pathlib import Path + +from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QFileDialog, \ + QCheckBox, QComboBox + + +class ExportKontorDialog(QDialog): + def __init__(self, parent=None, kontor_db=None): + super().__init__(parent) + + self.parent = parent + self.kontor_db = kontor_db + self.file_name = "data.json" + self.tables = [] + self._table_options = {} + + self.export_options = {"JSON": {"ext": ".json"}, "YAML": {"ext": ".yaml"}, "SQLite": {"ext": ".db"}} + self.current_export_type = "JSON" + + buttons = (QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + + self.buttonBox = QDialogButtonBox(buttons) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + layout = QVBoxLayout() + + self.label = QLabel() + self.label.setText("Export DB to data.json") + + self.combo_box = QComboBox() + self.combo_box.addItems(["JSON", "YAML", "SQLite"]) + self.combo_box.currentTextChanged.connect(self.change_export_type) + file_layout = QHBoxLayout() + file_layout.addWidget(self.label) + file_layout.addWidget(self.combo_box) + file_button = QPushButton("Select file") + file_button.clicked.connect(self.select_file) + file_layout.addWidget(file_button) + layout.addLayout(file_layout) + + for table_name in self.kontor_db.get_table_names(): + check_box = QCheckBox(table_name) + check_box.setChecked(True) + self.tables.append(table_name) + self._table_options[table_name] = check_box + check_box.stateChanged.connect(self.change_selection) + layout.addWidget(check_box) + layout.addWidget(self.buttonBox) + self.setLayout(layout) + + def change_selection(self): + self.tables.clear() + for (name, box) in self._table_options.items(): + if box.isChecked(): + self.tables.append(name) + + def change_export_type(self, text): + self.current_export_type = text + self.label.setText(f'Export DB to data.{self.export_options[text]["ext"]}') + + def select_file(self): + file_dialog = QFileDialog() + file_dialog.setFileMode(QFileDialog.FileMode.AnyFile) + file_dialog.setDefaultSuffix(self.export_options[self.current_export_type]["ext"]) + file_dialog.setNameFilter(f'*{self.export_options[self.current_export_type]["ext"]}') + if file_dialog.exec(): + self.file_name = file_dialog.selectedFiles()[0] + export_file = Path(self.file_name) + self.file_name = export_file.with_suffix(self.export_options[self.current_export_type]["ext"]) + self.label.setText(f"Export DB to {self.file_name}") + + def get_tables_to_export(self) -> list: + return self.tables + + +class ImportKontorDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + + self.file_name = None + + QBtn = (QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + + self.buttonBox = QDialogButtonBox(QBtn) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + self.label = QLabel() + self.label.setText("Import DB from data.json") + layout = QVBoxLayout() + file_layout = QHBoxLayout() + file_layout.addWidget(self.label) + file_button = QPushButton("Select file") + file_button.clicked.connect(self.select_file) + file_layout.addWidget(file_button) + layout.addLayout(file_layout) + layout.addWidget(self.buttonBox) + self.setLayout(layout) + + def select_file(self): + file_dialog = QFileDialog() + file_dialog.setFileMode(QFileDialog.FileMode.ExistingFile) + if file_dialog.exec(): + self.file_name = file_dialog.selectedFiles()[0] + self.label.setText(f"Import DB from {self.file_name}") diff --git a/python/qt/gui/main_window.py b/qt/gui/main_window.py similarity index 100% rename from python/qt/gui/main_window.py rename to qt/gui/main_window.py diff --git a/python/qt/gui/model_config.py b/qt/gui/model_config.py similarity index 100% rename from python/qt/gui/model_config.py rename to qt/gui/model_config.py diff --git a/python/qt/gui/table_model.py b/qt/gui/table_model.py similarity index 100% rename from python/qt/gui/table_model.py rename to qt/gui/table_model.py diff --git a/python/qt/kontor.py b/qt/kontor.py similarity index 100% rename from python/qt/kontor.py rename to qt/kontor.py diff --git a/python/qt/pysidedeploy.spec b/qt/pysidedeploy.spec old mode 100755 new mode 100644 similarity index 100% rename from python/qt/pysidedeploy.spec rename to qt/pysidedeploy.spec diff --git a/qt/res/application-export.png b/qt/res/application-export.png new file mode 100644 index 0000000000000000000000000000000000000000..555887a28d64bc812c4dfa98a6ff1da1927b7792 GIT binary patch literal 513 zcmV+c0{;DpP)A0|fCeqz@KJVX8nnt2D9k2UrOajFq_SwR4OUbbUJ;HgeDQF66oE777mBiWdd2F(NKOO zk$Ax17hTui{#6jPXfz7dY87I!*bgRr4TVA=afmd=7~nkYBq$b(su#^>Q?2y;%rJ~= zA;j4sgM^B|5YOb(M4cc`5q!^h_wPg5is0Dq{42l!6{r`}{QE*r00000NkvXXu0mjf Djqud07&@TR)18#v5Hqj8@|Bnpn>z;V`CueWd< z7oI05i2^A#D9Qz9v#hs`;>*D7{eB7>0ppH2SLstJO-8BwR~M`A7kF ztyTlI66tgrtyT-MSPa~yU>3AuKR54&OXJih@;oco-=1sDLQa}`@LC7Iy> O0000dz zNLw#yfl;S$OS+NxP!T0`q27a_KcR=-dJ5{Xm+WEf!J>k|BqE5#C}ImGwQ$$<(wdh& z>AIJ5zVFx9lo8J{@aWY%oIP6CAZKYHol)uRf|554m_YTnpAlGl zL9{5M4)qVt#u>|mLQVrh46q|<)MP|~t8EE-8crSv>~io{4+abW2A~lV0z>OM)mbBD zmWtqV;+?9YGlhpQQ@M9zi{*KRG_T5PQ_nvC9tMo_mT6XEIU5;J+S?d(HD_0f!1ES7 z7@Uj?4!kD>&?r8e&c1jan^9Cs!}3z0d7Izafj)-}rbYZR}EdihS6`M#Fcy= zaw{u#mV;BNWe3{Sqiq;{_M>cOP*8MhP1CLQf3t|C2tpM55ycYh7RL%}Yf+Bp#e!~C z2(WG^ZnlxC>igv!sW2GM8SslVJ(oFwkp~5*6E^0rT$Q zya}^2lB>ITrX0zGEa!lc90$HC4~#^g#%ZB2j3vzHhi;TO$n{?Wwx(^0>^QA_x~H^z zTf=2SxY0rCf|fl@rb7TXYr%pE(0=u@G literal 0 HcmV?d00001 diff --git a/qt/res/cross.png b/qt/res/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..6b9fa6dd36ee8165272a13dd263f573507c78ca6 GIT binary patch literal 544 zcmV+*0^j|KP)L-ku(! z6_?D-?!0+#=VtbVZQGdTnZt~apI_HPK#=zVadHM((E`e*C+RoFb)Qo8-U{Lb7{`Tz zw4B8FZ|o?Wox+p=1wp47C;7ar*Xu~;a?<=sjPv>+otDjJ6FaGt!YnNyxQQhp)G1V! zk;r6ZtyV)c8pTbiRAnHRNXTxti%=+p$4aG2*+mMM&xrdiU^`VPk^N*+HX98^7!HT% zwA%;A{T)GxeQ|RcxyzV%! z$AbYD+)i1R64zB?q}NmTz}5|04~J#YG_goA*Eq(QJvp7pG14hUj1pZ^t>3S*xqHUO ze~r;}zTY_XkZ*}d@gm!;N90h8nBQenBUZ_ukZKNicn*hc_Pmc!Jn|2=s<~}>!Sb>Qm3!QP46b_JFw5YU70Tu$X}=T}ez@@t%j iG9vD)nDux55?}x$+|UyQVK_bj0000tYd4K$mX5uyr2F@fdffp{&DSHtl4|Bn9=ukpCx`#%PTUr-D(@b7;C zhJXJjrnw{;1KBM=6&|E`fsNrGL!Y6%zUh}QUl`(@V)PmQFtotEKmafTHP0uP}7&%m4pcKQ#X)BiAJ2y+PrtBI>9eEIt2-_c7)?*Lsf z5vX5*Af@m-wBJRV%#Fiz=BdPr0!2^a2d-yv7gSU);Z6{`~Oy?E5qSnHUln4*Y%!){F!Y1~5WXoC44g zb7l_)X~q^V6MmWR7e3wj|L|Wb!|^}Y86N$^h+kv_Kw-fP!~#If$6(B4$)LlK#&G6; zC&ShMSAb$5tA9Xg5dOsg@-z^@3;;QS9f&!gG&3|;{Da~@Q2ZB({s%XJ5&#fj0J|In U+D>(nEdT%j07*qoM6N<$g0@&8X#fBK literal 0 HcmV?d00001 From 4cea5541160e32fe449dd2be34de5f42479a9e64 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Tue, 14 Jan 2025 17:53:05 +0100 Subject: [PATCH 46/91] add missing schema classes --- python/kontor/database/__init__.py | 11 ++++- python/kontor/database/bookshelf.py | 75 +++++++++++++++++++++++++++++ python/kontor/database/media.py | 28 ++++++++++- 3 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 python/kontor/database/bookshelf.py diff --git a/python/kontor/database/__init__.py b/python/kontor/database/__init__.py index c7e6cb5..f436cbe 100644 --- a/python/kontor/database/__init__.py +++ b/python/kontor/database/__init__.py @@ -3,10 +3,11 @@ from datetime import datetime from pathlib import Path from .base import Base +from .bookshelf import Article, Book, Author, BookshelfPublisher, ArticleAuthor, BookAuthor from .comic import Comic, Artist, Publisher, Issue, StoryArc, TradePaperback, Volume, ComicWork, WorkType from .metadata import MetaDataTable, MetaDataColumn from .tysc import Card, CardSet, Sport, Team, FieldPosition, Rooster, Player, Vendor -from .media import MediaFile +from .media import MediaFile, MediaArticle, MediaVideo class KontorDB: @@ -35,7 +36,15 @@ class KontorDB: self.registry['volume'] = Volume self.registry['comic_work'] = ComicWork self.registry['worktype'] = WorkType + self.registry['article'] = Article + self.registry['book'] = Book + self.registry['author'] = Author + self.registry['bookshelf_publisher'] = BookshelfPublisher + self.registry['article_author'] = ArticleAuthor + self.registry['book_author'] = BookAuthor self.registry['media_file'] = MediaFile + self.registry['media_article'] = MediaArticle + self.registry['media_video'] = MediaVideo def get_table_names(self) -> list: tables = self.session.query(MetaDataTable).all() diff --git a/python/kontor/database/bookshelf.py b/python/kontor/database/bookshelf.py new file mode 100644 index 0000000..0e09fd0 --- /dev/null +++ b/python/kontor/database/bookshelf.py @@ -0,0 +1,75 @@ +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from .base import Base + + +class Article(Base): + __tablename__ = 'article' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + title = Column(String(length=255), unique=True) + article_authors = relationship("ArticleAuthor") + + +class Author(Base): + __tablename__ = 'author' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + first_name = Column(String(255)) + last_name = Column(String(255)) + article_authors = relationship("ArticleAuthor") + book_authors = relationship("BookAuthor") + + +class BookshelfPublisher(Base): + __tablename__ = 'bookshelf_publisher' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), unique=True) + books = relationship("Book") + + +class Book(Base): + __tablename__ = 'book' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + isbn = Column(String(255), unique=True) + title = Column(String(255)) + year = Column(Integer, nullable=False) + publisher_id = Column(String, ForeignKey('bookshelf_publisher.id'), nullable=False) + publisher = relationship('BookshelfPublisher', back_populates="books") + book_authors = relationship("BookAuthor") + + +class ArticleAuthor(Base): + __tablename__ = 'article_author' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + article_id = Column(String, ForeignKey('article.id'), nullable=False) + article = relationship('Article', back_populates="article_authors") + author_id = Column(String, ForeignKey('author.id'), nullable=False) + author = relationship('Author', back_populates="article_authors") + + +class BookAuthor(Base): + __tablename__ = 'book_author' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + author_id = Column(String, ForeignKey('author.id'), nullable=False) + author = relationship('Author', back_populates="book_authors") + book_id = Column(String, ForeignKey('book.id'), nullable=False) + book = relationship('Book', back_populates="book_authors") diff --git a/python/kontor/database/media.py b/python/kontor/database/media.py index 0129c03..85e6155 100644 --- a/python/kontor/database/media.py +++ b/python/kontor/database/media.py @@ -15,7 +15,7 @@ class MediaFile(Base): path = Column(String(255)) review = Column(BIT(1)) title = Column(String(255)) - url = Column(String(255)) + url = Column(String(255), unique=True) should_download = Column(BIT(1)) def __repr__(self): @@ -23,3 +23,29 @@ class MediaFile(Base): def __str__(self): return f'{self.title}({self.id})' + + +class MediaArticle(Base): + __tablename__ = 'media_article' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + review = Column(BIT(1)) + title = Column(String(255)) + url = Column(String(255), unique=True) + + +class MediaVideo(Base): + __tablename__ = 'media_video' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + cloud_link = Column(String(255)) + file_name = Column(String(255)) + path = Column(String(255)) + review = Column(BIT(1)) + title = Column(String(255)) + url = Column(String(255), unique=True) + should_download = Column(BIT(1)) From 3466c10a88ab5aca69f60e18aa31d6c102e7aa50 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Tue, 14 Jan 2025 22:58:15 +0100 Subject: [PATCH 47/91] add commands and subcommands --- python/kontor/controllers/clibase.py | 7 +++-- python/kontor/controllers/database.py | 23 ++++++++++++++ python/kontor/controllers/media.py | 40 +++++++++++++++++++++++++ python/kontor/database/__init__.py | 3 ++ python/kontor/main.py | 11 +++++-- python/kontor/templates/download.jinja2 | 2 ++ python/kontor/templates/import.jinja2 | 2 ++ python/kontor/templates/update.jinja2 | 2 ++ 8 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 python/kontor/controllers/media.py create mode 100644 python/kontor/templates/download.jinja2 create mode 100644 python/kontor/templates/import.jinja2 create mode 100644 python/kontor/templates/update.jinja2 diff --git a/python/kontor/controllers/clibase.py b/python/kontor/controllers/clibase.py index 568a546..6542f9e 100644 --- a/python/kontor/controllers/clibase.py +++ b/python/kontor/controllers/clibase.py @@ -18,7 +18,7 @@ class CliBase(Controller): description = 'Kontor CLI Tool' # text displayed at the bottom of --help output - epilog = 'Usage: kontor gui|database' + epilog = 'Usage: kontor (gui | database | media) [subcommands]' # controller level arguments. ex: 'kontor --version' arguments = [ @@ -26,11 +26,14 @@ class CliBase(Controller): (['-v', '--version'], {'action': 'version', 'version': VERSION_BANNER}), + (['-m', '--dry-run'], + {'action': 'store_true', + 'dest': 'dry_run'}) ] def _default(self): """Default action if no sub-command is passed.""" - self.gui() + self.app.args.print_help() @ex( help='start GUI' diff --git a/python/kontor/controllers/database.py b/python/kontor/controllers/database.py index 246d4f8..2f3fde6 100644 --- a/python/kontor/controllers/database.py +++ b/python/kontor/controllers/database.py @@ -30,3 +30,26 @@ class Database(Controller): table_list = kontor_db.get_table_names() kontor_db.export_db(data['export_type'], data['db_file'], table_list) self.app.render(data, 'command1.jinja2') + + @ex( + label='import', + help='import data from file into database', + arguments=[ + (['-f', '--file'], + {'help': 'file to read data', + 'action': 'store', + 'dest': 'db_file'}) + ], + ) + def import_cmd(self): + data = { + 'db_file': 'data.json', + 'data_type': 'JSON', + } + if self.app.pargs.db_file is not None: + data['db_file'] = self.app.pargs.db_file + kontor_db = KontorDB(self.app.session, self.app.log) + if self.app.pargs.dry_run: + self.app.render(data, 'import.jinja2') + else: + kontor_db.import_db(data['db_file']) diff --git a/python/kontor/controllers/media.py b/python/kontor/controllers/media.py new file mode 100644 index 0000000..dbb5a70 --- /dev/null +++ b/python/kontor/controllers/media.py @@ -0,0 +1,40 @@ +from cement import Controller, ex + + +class Media(Controller): + + class Meta: + label = 'media' + stacked_type = 'nested' + stacked_on = 'base' + + @ex( + label='update', + help='update title for mediafiles', + ) + def update_title(self): + if self.app.pargs.dry_run: + print('print command to shell') + self.app.render({}, 'update.jinja2') + + + + @ex( + label='download', + help='download all marked videos', + arguments=[ + (['-d', '--dir'], + {'help': 'directory to store videos', + 'action': 'store', + 'dest': 'media_dir'}) + ], + ) + def download(self): + data = { + 'media_dir': '/data/media', + } + if self.app.pargs.media_dir is not None: + data['media_dir'] = self.app.pargs.media_dir + if self.app.pargs.dry_run: + print('print command to shell') + self.app.render(data, 'download.jinja2') diff --git a/python/kontor/database/__init__.py b/python/kontor/database/__init__.py index f436cbe..31fc94a 100644 --- a/python/kontor/database/__init__.py +++ b/python/kontor/database/__init__.py @@ -153,3 +153,6 @@ class KontorDB: self.log.debug("unknown export type") if export_file.exists(): self.log.debug(f"{export_file} exists") + + def import_db(self, import_file_name: str): + pass diff --git a/python/kontor/main.py b/python/kontor/main.py index aa71e06..98a20b4 100644 --- a/python/kontor/main.py +++ b/python/kontor/main.py @@ -3,20 +3,23 @@ from cement.core.exc import CaughtSignal from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker +from kontor.controllers.media import Media from .core.exc import KontorError from .database.base import Base from .controllers.clibase import CliBase from .controllers.database import Database # configuration defaults -CONFIG = init_defaults('kontor', 'mariadb') +CONFIG = init_defaults('kontor', 'mariadb','output.json', 'output.yaml') CONFIG['kontor']['foo'] = 'bar' CONFIG['mariadb']['user'] = 'kontor' CONFIG['mariadb']['password'] = 'kontor' CONFIG['mariadb']['host'] = '127.0.0.1' CONFIG['mariadb']['port'] = '3306' CONFIG['mariadb']['database'] = 'kontor' - +META = init_defaults('output.json', 'output.yaml') +META['output.json']['overridable'] = True +META['output.yaml']['overridable'] = True def extend_sqlalchemy(app): app.log.info('extending kontor application with sqlalchemy') @@ -48,12 +51,15 @@ class Kontor(App): # configuration defaults config_defaults = CONFIG + meta_defaults = META + # call sys.exit() on close exit_on_close = True # load additional framework extensions extensions = [ 'yaml', + 'json', 'colorlog', 'jinja2', ] @@ -78,6 +84,7 @@ class Kontor(App): handlers = [ CliBase, Database, + Media, ] diff --git a/python/kontor/templates/download.jinja2 b/python/kontor/templates/download.jinja2 new file mode 100644 index 0000000..a6e3d50 --- /dev/null +++ b/python/kontor/templates/download.jinja2 @@ -0,0 +1,2 @@ + +Download videos to directory {{ media_dir }} diff --git a/python/kontor/templates/import.jinja2 b/python/kontor/templates/import.jinja2 new file mode 100644 index 0000000..c54f162 --- /dev/null +++ b/python/kontor/templates/import.jinja2 @@ -0,0 +1,2 @@ + +Import data from {{ db_file }} diff --git a/python/kontor/templates/update.jinja2 b/python/kontor/templates/update.jinja2 new file mode 100644 index 0000000..992873f --- /dev/null +++ b/python/kontor/templates/update.jinja2 @@ -0,0 +1,2 @@ + +Update entries of mediafile From f3cf1a17f32154055bcddace1c6be973e68e4a5c Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Wed, 15 Jan 2025 14:16:35 +0100 Subject: [PATCH 48/91] add import functionality --- python/kontor/controllers/database.py | 6 +-- python/kontor/database/__init__.py | 62 ++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/python/kontor/controllers/database.py b/python/kontor/controllers/database.py index 2f3fde6..02a565e 100644 --- a/python/kontor/controllers/database.py +++ b/python/kontor/controllers/database.py @@ -49,7 +49,5 @@ class Database(Controller): if self.app.pargs.db_file is not None: data['db_file'] = self.app.pargs.db_file kontor_db = KontorDB(self.app.session, self.app.log) - if self.app.pargs.dry_run: - self.app.render(data, 'import.jinja2') - else: - kontor_db.import_db(data['db_file']) + self.app.render(data, 'import.jinja2') + kontor_db.import_db(data['db_file'], self.app.pargs.dry_run) diff --git a/python/kontor/database/__init__.py b/python/kontor/database/__init__.py index 31fc94a..f62097b 100644 --- a/python/kontor/database/__init__.py +++ b/python/kontor/database/__init__.py @@ -154,5 +154,63 @@ class KontorDB: if export_file.exists(): self.log.debug(f"{export_file} exists") - def import_db(self, import_file_name: str): - pass + def import_db(self, import_file_name: str, dry_run: bool): + import_file = Path(import_file_name) + if not import_file.exists(): + print(f"File {import_file_name} does not exist. Do nothing.") + return + self.log.debug(f"evaluate type from file extension: {import_file.suffix}") + match import_file.suffix: + case '.json': + print("read json file") + with open(import_file_name, 'r') as json_file: + json_load = json.load(json_file) + for table in json_load: + print(f"{table}: {len(json_load[table])}") + self.import_table(table, json_load[table], dry_run) + case '.yml': + print("read yaml file") + case '.yaml': + print("read yaml file") + case '.db': + print("read sqlite file") + + def import_table(self, table_name, items, dry_run: bool): + existing_ids = self.get_ids(table_name) + for item in items: + self.log.debug(f"{item}") + current_id = item['id'] + found_item = self.session.query(self.registry[table_name]).get(current_id) + self.log.debug(f"found: {found_item}") + if found_item is not None: + changed = self.update_entry(found_item, item, dry_run) + if changed: + print(f"{current_id} has changed") + existing_ids.remove(current_id) + if len(existing_ids) > 0: + print("remaining items") + self.session.commit() + + def get_ids(self, table_name: str) -> list: + existing_ids = [] + items = self.session.query(self.registry[table_name]).all() + for item in items: + existing_ids.append(getattr(item, 'id')) + return existing_ids + + def update_entry(self, existing_item, update_item: dict, dry_run: bool) -> bool: + changed = False + for key in update_item.keys(): + update_value = update_item[key] + existing_value = getattr(existing_item, key) + if type(existing_value) is not type(update_value): + self.log.debug(f"compare {type(existing_value)} with {type(update_value)}") + existing_value = str(existing_value) + if existing_value != update_value: + print(f"{key} has changed: {existing_value} != {update_value}") + if not dry_run: + setattr(existing_item, key, update_value) + # existing_item[key] = update_value + changed = True + self.log.info(f"update {key} with {update_value}") + return changed From 33dcbc4413470b17ef989b8dc682bfc82d424f92 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Thu, 16 Jan 2025 16:50:14 +0100 Subject: [PATCH 49/91] add command to add link --- python/kontor/controllers/media.py | 25 +++++++++++++++++++++++++ python/kontor/database/__init__.py | 3 +++ 2 files changed, 28 insertions(+) diff --git a/python/kontor/controllers/media.py b/python/kontor/controllers/media.py index dbb5a70..dbc9eb5 100644 --- a/python/kontor/controllers/media.py +++ b/python/kontor/controllers/media.py @@ -1,5 +1,7 @@ from cement import Controller, ex +from ..database import KontorDB + class Media(Controller): @@ -38,3 +40,26 @@ class Media(Controller): if self.app.pargs.dry_run: print('print command to shell') self.app.render(data, 'download.jinja2') + + @ex( + help='add url to database', + arguments=[ + (['-u', '--url'], + {'help': 'link to downloadable video', + 'action': 'store', + 'dest': 'link'}) + ], + ) + def add(self): + data = { + 'link_url': None + } + if self.app.pargs.link is not None: + data['link_url'] = self.app.pargs.link + if self.app.pargs.dry_run: + print(f"add url {data['link_url']} to database") + kontor_db = KontorDB(self.app.session, self.app.log) + kontor_db.add_link(self.app.pargs.link, self.app.pargs.dry_run) + + else: + print("no url was given.") diff --git a/python/kontor/database/__init__.py b/python/kontor/database/__init__.py index f62097b..667b870 100644 --- a/python/kontor/database/__init__.py +++ b/python/kontor/database/__init__.py @@ -214,3 +214,6 @@ class KontorDB: changed = True self.log.info(f"update {key} with {update_value}") return changed + + def add_link(self, link: str, dry_run: bool): + self.log.info(f"add link {link} to media_file") From 58263cb85465e47141f2e83ec232e47015437022 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Thu, 16 Jan 2025 23:03:30 +0100 Subject: [PATCH 50/91] use sqlalchemy session only as short as possible --- python/Dockerfile | 6 +- python/kontor/controllers/clibase.py | 2 +- python/kontor/controllers/database.py | 7 +- python/kontor/controllers/media.py | 2 +- python/kontor/database/__init__.py | 154 +++++++++++++++++--------- python/kontor/gui/main_window.py | 6 +- python/kontor/main.py | 1 + python/setup.py | 2 +- 8 files changed, 115 insertions(+), 65 deletions(-) diff --git a/python/Dockerfile b/python/Dockerfile index d32cf5c..a3e005d 100644 --- a/python/Dockerfile +++ b/python/Dockerfile @@ -1,6 +1,10 @@ -FROM python:3.9-alpine +#FROM python:3.9-alpine +FROM python:3.11-bookworm LABEL MAINTAINER="Thomas Peetz " ENV PS1="\[\e[0;33m\]|> kontor <| \[\e[1;35m\]\W\[\e[0m\] \[\e[0m\]# " +ENV DEBIAN_FRONTEND noninteractive +RUN apt-get update && apt-get install -y apt-utils libmariadb3 libmariadb-dev +RUN rm -rf /var/lib/apt/lists/* WORKDIR /src COPY . /src diff --git a/python/kontor/controllers/clibase.py b/python/kontor/controllers/clibase.py index 6542f9e..d467413 100644 --- a/python/kontor/controllers/clibase.py +++ b/python/kontor/controllers/clibase.py @@ -40,6 +40,6 @@ class CliBase(Controller): ) def gui(self): application = QApplication([]) - window = MainWindow(self.app.session, self.app.log) + window = MainWindow(self.app.session, self.app.engine, self.app.log) window.show() application.exec() diff --git a/python/kontor/controllers/database.py b/python/kontor/controllers/database.py index 02a565e..b0be6eb 100644 --- a/python/kontor/controllers/database.py +++ b/python/kontor/controllers/database.py @@ -26,9 +26,8 @@ class Database(Controller): } if self.app.pargs.db_file is not None: data['db_file'] = self.app.pargs.db_file - kontor_db = KontorDB(self.app.session, self.app.log) - table_list = kontor_db.get_table_names() - kontor_db.export_db(data['export_type'], data['db_file'], table_list) + kontor_db = KontorDB(self.app.session, self.app.engine, self.app.log) + kontor_db.export_db(data['export_type'], data['db_file']) self.app.render(data, 'command1.jinja2') @ex( @@ -48,6 +47,6 @@ class Database(Controller): } if self.app.pargs.db_file is not None: data['db_file'] = self.app.pargs.db_file - kontor_db = KontorDB(self.app.session, self.app.log) + kontor_db = KontorDB(self.app.session, self.app.engine, self.app.log) self.app.render(data, 'import.jinja2') kontor_db.import_db(data['db_file'], self.app.pargs.dry_run) diff --git a/python/kontor/controllers/media.py b/python/kontor/controllers/media.py index dbc9eb5..2335b72 100644 --- a/python/kontor/controllers/media.py +++ b/python/kontor/controllers/media.py @@ -58,7 +58,7 @@ class Media(Controller): data['link_url'] = self.app.pargs.link if self.app.pargs.dry_run: print(f"add url {data['link_url']} to database") - kontor_db = KontorDB(self.app.session, self.app.log) + kontor_db = KontorDB(self.app.session, self.app.engine, self.app.log) kontor_db.add_link(self.app.pargs.link, self.app.pargs.dry_run) else: diff --git a/python/kontor/database/__init__.py b/python/kontor/database/__init__.py index 667b870..5feec11 100644 --- a/python/kontor/database/__init__.py +++ b/python/kontor/database/__init__.py @@ -1,7 +1,11 @@ import json +import uuid from datetime import datetime from pathlib import Path +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import sessionmaker + from .base import Base from .bookshelf import Article, Book, Author, BookshelfPublisher, ArticleAuthor, BookAuthor from .comic import Comic, Artist, Publisher, Issue, StoryArc, TradePaperback, Volume, ComicWork, WorkType @@ -12,8 +16,9 @@ from .media import MediaFile, MediaArticle, MediaVideo class KontorDB: - def __init__(self, db_session, log): + def __init__(self, db_session, db_engine, log): self.session = db_session + self.engine = db_engine self.log = log self.registry = {} self.init_registry() @@ -47,8 +52,11 @@ class KontorDB: self.registry['media_video'] = MediaVideo def get_table_names(self) -> list: - tables = self.session.query(MetaDataTable).all() - result = [table.table_name for table in tables] + result = [] + __session__ = sessionmaker(self.engine) + with __session__() as session: + tables = self.session.query(MetaDataTable).all() + result = [table.table_name for table in tables] return result def get_column_meta_data(self, table_name: str, view_only=True) -> dict: @@ -76,41 +84,46 @@ class KontorDB: def get_filters(self, table_name): _filter_map = {} - for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). - filter(MetaDataTable.id == MetaDataColumn.table_id). - filter(MetaDataTable.table_name == table_name). - filter(MetaDataColumn.show_filter == 1).all()): - _filter_map[column.column_name] = {'label': column.filter_label, 'widget': None} - self.log.info(f"retrieved {len(_filter_map)} filters: {_filter_map}") + __session__ = sessionmaker(self.engine) + with __session__() as session: + for (_, column) in (session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name). + filter(MetaDataColumn.show_filter == 1).all()): + _filter_map[column.column_name] = {'label': column.filter_label, 'widget': None} + self.log.debug(f"retrieved {len(_filter_map)} filters: {_filter_map}") return _filter_map def data(self, table, columns: dict, filters) -> list: data = [] - entries = [] - if len(filters) == 0: - entries = self.session.query(table).all() - else: - entries = self.session.query(table).filter_by(**filters) - for entry in entries: - row = [] - for order in columns.keys(): - column_name = columns[order]['column'] - if str(column_name).endswith("_id"): - ref_table = column_name[:-3] - # print(f"{ref_table=}") - ref = getattr(entry, ref_table) - value = getattr(ref, "name") - # print(f"{value=}") - row.append(value) - else: - row.append(getattr(entry, column_name)) - # print(repr(row)) - data.append(row) + __session__ = sessionmaker(self.engine) + with __session__() as session: + entries = [] + if len(filters) == 0: + entries = session.query(table).all() + else: + entries = session.query(table).filter_by(**filters) + for entry in entries: + row = [] + for order in columns.keys(): + column_name = columns[order]['column'] + if str(column_name).endswith("_id"): + ref_table = column_name[:-3] + # print(f"{ref_table=}") + ref = getattr(entry, ref_table) + value = getattr(ref, "name") + # print(f"{value=}") + row.append(value) + else: + row.append(getattr(entry, column_name)) + # print(repr(row)) + data.append(row) return data - def export_db(self, export_type: str, export_file_name: str, export_table_list: list): + def export_db(self, export_type: str, export_file_name: str): self.log.info(f"export DB to {export_file_name} as {export_type}") db = {} + export_table_list = self.get_table_names() for table in export_table_list: columns = self.get_column_meta_data(table, view_only=False) if table in self.registry: @@ -118,27 +131,29 @@ class KontorDB: else: print(f"table {table} is not registered") continue - rows = self.session.query(model).all() - entries = [] - self.log.debug(f"found {len(rows)} entries") - self.log.debug(f"found {len(columns)} columns") - for row in rows: - # print(row) - entry = {} - for order in columns: - # print(columns[order]) - column_name = columns[order]['column'] - # print(f"get value {column_name} from {row} of table {table}") - try: - value = getattr(row, column_name) - if isinstance(value, datetime): - entry[column_name] = str(value) - else: - entry[column_name] = value - except AttributeError as error: - self.log.debug("could not get value") - entries.append(entry) - db[table] = entries + __session__ = sessionmaker(self.engine) + with __session__() as session: + rows = session.query(model).all() + entries = [] + self.log.debug(f"found {len(rows)} entries") + self.log.debug(f"found {len(columns)} columns") + for row in rows: + # print(row) + entry = {} + for order in columns: + # print(columns[order]) + column_name = columns[order]['column'] + # print(f"get value {column_name} from {row} of table {table}") + try: + value = getattr(row, column_name) + if isinstance(value, datetime): + entry[column_name] = str(value) + else: + entry[column_name] = value + except AttributeError as error: + self.log.debug("could not get value") + entries.append(entry) + db[table] = entries export_file = Path(export_file_name) match export_type: case "JSON": @@ -187,17 +202,31 @@ class KontorDB: if changed: print(f"{current_id} has changed") existing_ids.remove(current_id) + else: + self.log.info("item to import not found in database, add new one...") if len(existing_ids) > 0: print("remaining items") self.session.commit() def get_ids(self, table_name: str) -> list: existing_ids = [] - items = self.session.query(self.registry[table_name]).all() - for item in items: - existing_ids.append(getattr(item, 'id')) + __session__ = sessionmaker(self.engine) + with __session__() as session: + items = session.query(self.registry[table_name]).all() + for item in items: + existing_ids.append(getattr(item, 'id')) return existing_ids + def add_entry(self, update_item: dict): + media_file = MediaFile() + __session__ = sessionmaker(self.engine) + with __session__() as session: + for key in update_item.keys(): + update_value = update_item[key] + setattr(media_file, key, update_value) + session.add(media_file) + session.commit() + def update_entry(self, existing_item, update_item: dict, dry_run: bool) -> bool: changed = False for key in update_item.keys(): @@ -217,3 +246,20 @@ class KontorDB: def add_link(self, link: str, dry_run: bool): self.log.info(f"add link {link} to media_file") + __session__ = sessionmaker(self.engine) + with __session__() as session: + media_file = MediaFile() + media_file.id = str(uuid.uuid4()) + media_file.created_date = datetime.now() + media_file.last_modified_date = datetime.now() + media_file.version = 0 + media_file.url = link + media_file.review = 1 + media_file.should_download = 1 + try: + session.add(media_file) + session.commit() + self.log.info(f"entry {media_file} successfully added") + except IntegrityError as error: + self.session.rollback() + self.log.info(error.orig) diff --git a/python/kontor/gui/main_window.py b/python/kontor/gui/main_window.py index 24a84a6..6e775bc 100644 --- a/python/kontor/gui/main_window.py +++ b/python/kontor/gui/main_window.py @@ -12,7 +12,7 @@ from .table_model import KontorTableModel class MainWindow(QMainWindow): - def __init__(self, session, log): + def __init__(self, session, engine, log): super().__init__() self.tick = QIcon('kontor/res/tick.png') @@ -30,7 +30,7 @@ class MainWindow(QMainWindow): self.data = [] self.filter = {} - self.kontor_db = KontorDB(session, log) + self.kontor_db = KontorDB(session, engine, log) self.log = log self.central_widget = QWidget() parent_layout = QVBoxLayout() @@ -114,7 +114,7 @@ class MainWindow(QMainWindow): self.log.info(export_dlg.get_tables_to_export()) self.log.info(f"export DB to {export_dlg.file_name}") self.statusBar.showMessage(f"export DB to {export_dlg.file_name}", 3000) - self.kontor_db.export_db(export_dlg.current_export_type, export_dlg.file_name, export_dlg.get_tables_to_export()) + self.kontor_db.export_db(export_dlg.current_export_type, export_dlg.file_name) else: self.statusBar.showMessage("Export cancelled", 3000) diff --git a/python/kontor/main.py b/python/kontor/main.py index 98a20b4..9460b56 100644 --- a/python/kontor/main.py +++ b/python/kontor/main.py @@ -35,6 +35,7 @@ def extend_sqlalchemy(app): Base.metadata.create_all(bind=engine) __session__ = sessionmaker(bind=engine) app.extend('session', __session__()) + app.extend('engine', engine) def close_session(app): diff --git a/python/setup.py b/python/setup.py index 0b92a43..2f6b226 100644 --- a/python/setup.py +++ b/python/setup.py @@ -19,7 +19,7 @@ setup( url='https://gitlab.com/tpeetz/kontor', license='MIT', packages=find_packages(exclude=['ez_setup', 'tests*']), - package_data={'kontor': ['templates/*']}, + package_data={'kontor': ['templates/*', 'res/*',]}, include_package_data=True, entry_points=""" [console_scripts] From 611d6b9e612676a781254b1d18d1d1ee334f04b7 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Fri, 17 Jan 2025 13:31:49 +0100 Subject: [PATCH 51/91] add update and download for MediaFile --- python/kontor/controllers/clibase.py | 2 +- python/kontor/controllers/database.py | 4 +- python/kontor/controllers/media.py | 14 +- python/kontor/database/__init__.py | 177 +++++++++++++++++++------- python/kontor/database/base.py | 17 ++- python/kontor/database/bookshelf.py | 38 +----- python/kontor/database/comic.py | 62 +++------ python/kontor/database/media.py | 20 +-- python/kontor/database/metadata.py | 14 +- python/kontor/database/tysc.py | 51 ++------ python/kontor/gui/main_window.py | 20 ++- python/kontor/main.py | 12 +- python/requirements.txt | 1 + 13 files changed, 221 insertions(+), 211 deletions(-) diff --git a/python/kontor/controllers/clibase.py b/python/kontor/controllers/clibase.py index d467413..0491128 100644 --- a/python/kontor/controllers/clibase.py +++ b/python/kontor/controllers/clibase.py @@ -40,6 +40,6 @@ class CliBase(Controller): ) def gui(self): application = QApplication([]) - window = MainWindow(self.app.session, self.app.engine, self.app.log) + window = MainWindow(self.app.engine, self.app.config, self.app.log) window.show() application.exec() diff --git a/python/kontor/controllers/database.py b/python/kontor/controllers/database.py index b0be6eb..04fe2fb 100644 --- a/python/kontor/controllers/database.py +++ b/python/kontor/controllers/database.py @@ -26,7 +26,7 @@ class Database(Controller): } if self.app.pargs.db_file is not None: data['db_file'] = self.app.pargs.db_file - kontor_db = KontorDB(self.app.session, self.app.engine, self.app.log) + kontor_db = KontorDB(self.app.engine, self.app.log) kontor_db.export_db(data['export_type'], data['db_file']) self.app.render(data, 'command1.jinja2') @@ -47,6 +47,6 @@ class Database(Controller): } if self.app.pargs.db_file is not None: data['db_file'] = self.app.pargs.db_file - kontor_db = KontorDB(self.app.session, self.app.engine, self.app.log) + kontor_db = KontorDB(self.app.engine, self.app.log) self.app.render(data, 'import.jinja2') kontor_db.import_db(data['db_file'], self.app.pargs.dry_run) diff --git a/python/kontor/controllers/media.py b/python/kontor/controllers/media.py index 2335b72..e39c724 100644 --- a/python/kontor/controllers/media.py +++ b/python/kontor/controllers/media.py @@ -15,11 +15,8 @@ class Media(Controller): help='update title for mediafiles', ) def update_title(self): - if self.app.pargs.dry_run: - print('print command to shell') - self.app.render({}, 'update.jinja2') - - + kontor_db = KontorDB(self.app.engine, self.app.config, self.app.log) + kontor_db.update_title(self.app.pargs.dry_run) @ex( label='download', @@ -37,9 +34,8 @@ class Media(Controller): } if self.app.pargs.media_dir is not None: data['media_dir'] = self.app.pargs.media_dir - if self.app.pargs.dry_run: - print('print command to shell') - self.app.render(data, 'download.jinja2') + kontor_db = KontorDB(self.app.engine, self.app.config, self.app.log) + kontor_db.download_file(self.app.pargs.dry_run) @ex( help='add url to database', @@ -58,7 +54,7 @@ class Media(Controller): data['link_url'] = self.app.pargs.link if self.app.pargs.dry_run: print(f"add url {data['link_url']} to database") - kontor_db = KontorDB(self.app.session, self.app.engine, self.app.log) + kontor_db = KontorDB(self.app.engine, self.app.config, self.app.log) kontor_db.add_link(self.app.pargs.link, self.app.pargs.dry_run) else: diff --git a/python/kontor/database/__init__.py b/python/kontor/database/__init__.py index 5feec11..0c524fa 100644 --- a/python/kontor/database/__init__.py +++ b/python/kontor/database/__init__.py @@ -1,8 +1,15 @@ import json +import re +import subprocess import uuid from datetime import datetime from pathlib import Path +from typing import Any +import requests +from bs4 import BeautifulSoup +from cement.core.config import ConfigHandler +from sqlalchemy import Engine from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import sessionmaker @@ -16,9 +23,9 @@ from .media import MediaFile, MediaArticle, MediaVideo class KontorDB: - def __init__(self, db_session, db_engine, log): - self.session = db_session + def __init__(self, db_engine: Engine, config: ConfigHandler, log): self.engine = db_engine + self.config = config self.log = log self.registry = {} self.init_registry() @@ -50,37 +57,41 @@ class KontorDB: self.registry['media_file'] = MediaFile self.registry['media_article'] = MediaArticle self.registry['media_video'] = MediaVideo + self.registry['meta_data_table'] = MetaDataTable + self.registry[MetaDataColumn.__tablename__] = MetaDataColumn def get_table_names(self) -> list: result = [] __session__ = sessionmaker(self.engine) with __session__() as session: - tables = self.session.query(MetaDataTable).all() + tables = session.query(MetaDataTable).all() result = [table.table_name for table in tables] return result def get_column_meta_data(self, table_name: str, view_only=True) -> dict: meta_data = {} order = 0 - if view_only: - for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). - filter(MetaDataTable.id == MetaDataColumn.table_id). - filter(MetaDataTable.table_name == table_name). - filter(MetaDataColumn.is_shown == 1).all()): - meta_data[order] = {'column': column.column_name, 'label': column.column_label, - 'order': column.column_order, 'ref_column': column.ref_column} - order += 1 - else: - for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). - filter(MetaDataTable.id == MetaDataColumn.table_id). - filter(MetaDataTable.table_name == table_name).all()): - meta_data[order] = { - 'column': column.column_name, - 'order': column.column_order, - 'ref_column': column.ref_column - } - order += 1 - return meta_data + __session__ = sessionmaker(self.engine) + with __session__() as session: + if view_only: + for (_, column) in (session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name). + filter(MetaDataColumn.is_shown == 1).all()): + meta_data[order] = {'column': column.column_name, 'label': column.column_label, + 'order': column.column_order, 'ref_column': column.ref_column} + order += 1 + else: + for (_, column) in (session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name).all()): + meta_data[order] = { + 'column': column.column_name, + 'order': column.column_order, + 'ref_column': column.ref_column + } + order += 1 + return meta_data def get_filters(self, table_name): _filter_map = {} @@ -193,20 +204,23 @@ class KontorDB: def import_table(self, table_name, items, dry_run: bool): existing_ids = self.get_ids(table_name) for item in items: - self.log.debug(f"{item}") + # self.log.debug(f"{item}") current_id = item['id'] - found_item = self.session.query(self.registry[table_name]).get(current_id) - self.log.debug(f"found: {found_item}") - if found_item is not None: - changed = self.update_entry(found_item, item, dry_run) - if changed: - print(f"{current_id} has changed") - existing_ids.remove(current_id) - else: - self.log.info("item to import not found in database, add new one...") + found_item = None + __session__ = sessionmaker(self.engine) + with __session__() as session: + found_item = session.query(self.registry[table_name]).get(current_id) + self.log.debug(f"found: {found_item}") + if found_item is not None: + changed = self.update_entry(found_item, item, dry_run) + if changed: + print(f"{current_id} has changed") + existing_ids.remove(current_id) + else: + self.log.info("item to import not found in database, add new one...") + self.add_entry(table_name, item, session, dry_run) if len(existing_ids) > 0: print("remaining items") - self.session.commit() def get_ids(self, table_name: str) -> list: existing_ids = [] @@ -217,14 +231,15 @@ class KontorDB: existing_ids.append(getattr(item, 'id')) return existing_ids - def add_entry(self, update_item: dict): - media_file = MediaFile() - __session__ = sessionmaker(self.engine) - with __session__() as session: - for key in update_item.keys(): - update_value = update_item[key] - setattr(media_file, key, update_value) - session.add(media_file) + def add_entry(self, table_name: str, update_item: dict, session, dry_run: bool): + add_item = self.registry[table_name]() + for key in update_item.keys(): + update_value = update_item[key] + setattr(add_item, key, update_value) + if dry_run: + self.log.info(f"add item {type(add_item)} with id {update_item['id']}") + else: + session.add(add_item) session.commit() def update_entry(self, existing_item, update_item: dict, dry_run: bool) -> bool: @@ -233,7 +248,7 @@ class KontorDB: update_value = update_item[key] existing_value = getattr(existing_item, key) if type(existing_value) is not type(update_value): - self.log.debug(f"compare {type(existing_value)} with {type(update_value)}") + # self.log.debug(f"compare {type(existing_value)} with {type(update_value)}") existing_value = str(existing_value) if existing_value != update_value: print(f"{key} has changed: {existing_value} != {update_value}") @@ -261,5 +276,81 @@ class KontorDB: session.commit() self.log.info(f"entry {media_file} successfully added") except IntegrityError as error: - self.session.rollback() + session.rollback() self.log.info(error.orig) + + def update_title(self, dry_run=False): + self.log.info(f"get links to review of media_file") + __session__ = sessionmaker(self.engine) + with __session__() as session: + links = session.query(MediaFile).filter(MediaFile.review == 1).all() + self.log.info(f"try to update {len(links)} items") + for link in links: + url = link.url + if url is None: + self.log.info(f"url has not been set for {link.id}") + continue + self.log.info('get title for url {}'.format(url)) + if dry_run: + continue + try: + r = requests.get(url) + soup = BeautifulSoup(r.content, "html.parser") + title = soup.title.string + except: + self.log.info("Sorry, could not retrieve title") + continue + self.log.info('ID {} has title {}'.format(link.id, title)) + link.title = title + link.review = 0 + session.commit() + + def download_file(self, dry_run=False): + self.log.info(f"download marked files of media_file") + __session__ = sessionmaker(self.engine) + with __session__() as session: + links = session.query(MediaFile).filter(MediaFile.should_download == 1).all() + self.log.info(f"try to download {len(links)} items") + for link in links: + url = link.url + if url is None: + self.log.info(f"url has not been set for {link.id}") + continue + if dry_run: + self.log.info(f"download {link.url} to {self.config.get('media', 'dir')}") + continue + filename = self.download_url(link) + if filename is None: + link.file_name = filename + link.should_download = 0 + session.commit() + + def parse_output(self, lines_list): + file_name = "" + for line in lines_list: + if 'has already been downloaded' in line: + end_len = len(' has already been downloaded') + file_name = line[11:-end_len] + self.log.info('found file: "%s"', file_name) + if 'Destination' in line: + line_len = len(line) + start_len = len('[download] Destination: ') + file_len = line_len - start_len + file_name = line[-file_len:] + self.log.info('new file: "%s"', file_name) + return file_name + + def download_url(self, video_url): + media_dir = Path(self.config.get('media', 'dir')) + if not media_dir.exists(): + media_dir = Path().absolute() + self.log.info(f"download video to {media_dir}") + result = subprocess.run([self.config.get('media', 'yt-dlp'), video_url], cwd=media_dir, capture_output=True, + text=True) + if result.returncode == 0: + output = result.stdout + output = re.sub(' +', ' ', output) + lines_list = output.splitlines() + return self.parse_output(lines_list) + else: + return None diff --git a/python/kontor/database/base.py b/python/kontor/database/base.py index fa2b68a..c976167 100644 --- a/python/kontor/database/base.py +++ b/python/kontor/database/base.py @@ -1,5 +1,20 @@ -from sqlalchemy.orm import DeclarativeBase +import uuid +from datetime import datetime + +from sqlalchemy import Integer, func, String +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column class Base(DeclarativeBase): pass + + +class BaseMixin: + # id = Column(String, primary_key=True) + id: Mapped[str] = mapped_column(primary_key=True, default=uuid.uuid4()) + # created_date = Column(DateTime) + created_date: Mapped[datetime] = mapped_column(default=func.now()) + # last_modified_date = Column(DateTime) + last_modified_date: Mapped[datetime] = mapped_column(default=func.now()) + # version = Column(Integer) + version: Mapped[int] = mapped_column(default=0) diff --git a/python/kontor/database/bookshelf.py b/python/kontor/database/bookshelf.py index 0e09fd0..ab0fe5a 100644 --- a/python/kontor/database/bookshelf.py +++ b/python/kontor/database/bookshelf.py @@ -2,47 +2,31 @@ from sqlalchemy import Column, DateTime, ForeignKey, Integer, String from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import relationship -from .base import Base +from .base import Base, BaseMixin -class Article(Base): +class Article(Base, BaseMixin): __tablename__ = 'article' - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) title = Column(String(length=255), unique=True) article_authors = relationship("ArticleAuthor") -class Author(Base): +class Author(Base, BaseMixin): __tablename__ = 'author' - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) first_name = Column(String(255)) last_name = Column(String(255)) article_authors = relationship("ArticleAuthor") book_authors = relationship("BookAuthor") -class BookshelfPublisher(Base): +class BookshelfPublisher(Base, BaseMixin): __tablename__ = 'bookshelf_publisher' - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) name = Column(String(length=255), unique=True) books = relationship("Book") -class Book(Base): +class Book(Base, BaseMixin): __tablename__ = 'book' - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) isbn = Column(String(255), unique=True) title = Column(String(255)) year = Column(Integer, nullable=False) @@ -51,24 +35,16 @@ class Book(Base): book_authors = relationship("BookAuthor") -class ArticleAuthor(Base): +class ArticleAuthor(Base, BaseMixin): __tablename__ = 'article_author' - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) article_id = Column(String, ForeignKey('article.id'), nullable=False) article = relationship('Article', back_populates="article_authors") author_id = Column(String, ForeignKey('author.id'), nullable=False) author = relationship('Author', back_populates="article_authors") -class BookAuthor(Base): +class BookAuthor(Base, BaseMixin): __tablename__ = 'book_author' - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) author_id = Column(String, ForeignKey('author.id'), nullable=False) author = relationship('Author', back_populates="book_authors") book_id = Column(String, ForeignKey('book.id'), nullable=False) diff --git a/python/kontor/database/comic.py b/python/kontor/database/comic.py index 49d222f..fe6ec19 100644 --- a/python/kontor/database/comic.py +++ b/python/kontor/database/comic.py @@ -2,15 +2,11 @@ from sqlalchemy import Column, DateTime, ForeignKey, Integer, String from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import relationship -from .base import Base +from .base import Base, BaseMixin -class Publisher(Base): +class Publisher(Base, BaseMixin): __tablename__ = "publisher" - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) name = Column(String(length=255), unique=True) comics = relationship("Comic") @@ -21,12 +17,8 @@ class Publisher(Base): return self.__repr__() -class Comic(Base): +class Comic(Base, BaseMixin): __tablename__ = 'comic' - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) title = Column(String(length=255), unique=True) publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False) publisher = relationship("Publisher", back_populates="comics") @@ -45,24 +37,16 @@ class Comic(Base): return f'{self.title}({self.id})' -class Volume(Base): +class Volume(Base, BaseMixin): __tablename__ = "volume" - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) name = Column(String(length=255), nullable=False) comic_id = Column(String, ForeignKey("comic.id"), nullable=False) comic = relationship("Comic", back_populates="volumes") issues = relationship("Issue") -class TradePaperback(Base): +class TradePaperback(Base, BaseMixin): __tablename__ = "trade_paperback" - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) name = Column(String(length=255), nullable=False) issue_start = Column(Integer) issue_end = Column(Integer) @@ -70,23 +54,15 @@ class TradePaperback(Base): comic = relationship("Comic", back_populates="trade_paperbacks") -class StoryArc(Base): +class StoryArc(Base, BaseMixin): __tablename__ = "story_arc" - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) name = Column(String(length=255), nullable=False) comic_id = Column(String, ForeignKey("comic.id"), nullable=False) comic = relationship("Comic", back_populates="story_arcs") -class Issue(Base): +class Issue(Base, BaseMixin): __tablename__ = "issue" - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) issue_number = Column(String(255)) in_stock = Column(BIT(1)) is_read = Column(BIT(1)) @@ -96,32 +72,26 @@ class Issue(Base): volume = relationship("Volume", back_populates="issues") -class Artist(Base): +class Artist(Base, BaseMixin): __tablename__ = "artist" - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) name = Column(String(length=255), nullable=False) comic_works = relationship("ComicWork") -class WorkType(Base): +class WorkType(Base, BaseMixin): __tablename__ = "worktype" - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) name = Column(String(length=255), nullable=False, unique=True) comic_works = relationship("ComicWork") + def __repr__(self): + return f'Worktype({self.id} {self.version} {self.name} {len(self.comic_works)})' -class ComicWork(Base): + def __str__(self): + return f'{self.name}({self.id})' + + +class ComicWork(Base, BaseMixin): __tablename__ = "comic_work" - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) comic_id = Column(String, ForeignKey("comic.id"), nullable=False) comic = relationship("Comic", back_populates="comic_works") artist_id = Column(String, ForeignKey("artist.id"), nullable=False) diff --git a/python/kontor/database/media.py b/python/kontor/database/media.py index 85e6155..d9d1bf2 100644 --- a/python/kontor/database/media.py +++ b/python/kontor/database/media.py @@ -1,15 +1,11 @@ from sqlalchemy import Column, DateTime, Integer, String from sqlalchemy.dialects.mysql import BIT -from .base import Base +from .base import Base, BaseMixin -class MediaFile(Base): +class MediaFile(Base, BaseMixin): __tablename__ = 'media_file' - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) cloud_link = Column(String(255)) file_name = Column(String(255)) path = Column(String(255)) @@ -25,23 +21,15 @@ class MediaFile(Base): return f'{self.title}({self.id})' -class MediaArticle(Base): +class MediaArticle(Base, BaseMixin): __tablename__ = 'media_article' - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) review = Column(BIT(1)) title = Column(String(255)) url = Column(String(255), unique=True) -class MediaVideo(Base): +class MediaVideo(Base, BaseMixin): __tablename__ = 'media_video' - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) cloud_link = Column(String(255)) file_name = Column(String(255)) path = Column(String(255)) diff --git a/python/kontor/database/metadata.py b/python/kontor/database/metadata.py index d7dd4c0..21d4d49 100644 --- a/python/kontor/database/metadata.py +++ b/python/kontor/database/metadata.py @@ -2,15 +2,11 @@ from sqlalchemy import Column, String, ForeignKey, DateTime, Integer, Boolean from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import relationship -from .base import Base +from .base import Base, BaseMixin -class MetaDataTable(Base): +class MetaDataTable(Base, BaseMixin): __tablename__ = 'meta_data_table' - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) table_name = Column(String(255), unique=True) table_columns = relationship("MetaDataColumn") @@ -21,12 +17,8 @@ class MetaDataTable(Base): return f'{self.table_name}({self.id})' -class MetaDataColumn(Base): +class MetaDataColumn(Base, BaseMixin): __tablename__ = 'meta_data_column' - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) column_modifier = Column(String(255), nullable=True) column_name = Column(String(255)) column_order = Column(Integer) diff --git a/python/kontor/database/tysc.py b/python/kontor/database/tysc.py index 733d7ed..ef8bc5d 100644 --- a/python/kontor/database/tysc.py +++ b/python/kontor/database/tysc.py @@ -2,29 +2,21 @@ from sqlalchemy import Column, DateTime, Integer, String, ForeignKey, UniqueCons from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import relationship -from .base import Base +from .base import Base, BaseMixin -class Sport(Base): +class Sport(Base, BaseMixin): __tablename__ = "sport" __table_args__ = ( UniqueConstraint("name"), ) - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) name = Column(String(255), nullable=False, index=True, unique=True) teams = relationship("Team") positions = relationship("FieldPosition") -class Team(Base): +class Team(Base, BaseMixin): __tablename__ = "team" - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) name = Column(String(255), nullable=False, index=True, unique=True) short_name = Column(String(255), nullable=False, ) sport_id = Column(String, ForeignKey("sport.id"), nullable=False) @@ -32,16 +24,12 @@ class Team(Base): roosters = relationship("Rooster") -class FieldPosition(Base): +class FieldPosition(Base, BaseMixin): __tablename__ = "field_position" __table_args__ = ( UniqueConstraint("name", "sport_id"), UniqueConstraint("short_name", "sport_id"), ) - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) name = Column(String(255), nullable=False, index=True) short_name = Column(String(255), nullable=False) sport_id = Column(String, ForeignKey("sport.id"), nullable=False, index=True) @@ -49,15 +37,11 @@ class FieldPosition(Base): roosters = relationship("Rooster") -class Player(Base): +class Player(Base, BaseMixin): __tablename__ = "player" __table_args__ = ( UniqueConstraint("first_name", "last_name"), ) - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) first_name = Column(String(255), nullable=False, index=True) last_name = Column(String(255), nullable=False, index=True) roosters = relationship("Rooster") @@ -66,15 +50,11 @@ class Player(Base): return f"{self.last_name}, {self.first_name}" -class Rooster(Base): +class Rooster(Base, BaseMixin): __tablename__ = "rooster" __table_args__ = ( UniqueConstraint("year", "team_id", "player_id", "position_id"), ) - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) year = Column(Integer) team_id = Column(String, ForeignKey("team.id"), nullable=False, index=True) team = relationship("Team", back_populates="roosters") @@ -84,26 +64,19 @@ class Rooster(Base): position = relationship("FieldPosition", back_populates="roosters") cards = relationship("Card") -class Vendor(Base): + +class Vendor(Base, BaseMixin): __tablename__ = "vendor" - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) name = Column(String(255), nullable=False, unique=True, index=True) card_sets = relationship("CardSet") cards = relationship("Card") -class CardSet(Base): +class CardSet(Base, BaseMixin): __tablename__ = "card_set" __table_args__ = ( UniqueConstraint("name", "vendor_id"), ) - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) name = Column(String(255), index=True) parallel_set = Column(BIT(1)) insert_set = Column(BIT(1)) @@ -112,15 +85,11 @@ class CardSet(Base): cards = relationship("Card") -class Card(Base): +class Card(Base, BaseMixin): __tablename__ = "card" __table_args__ = ( UniqueConstraint("card_number", "year", "vendor_id", "card_set_id"), ) - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) card_number = Column(Integer, index=True) year = Column(Integer, index=True) card_set_id = Column(String, ForeignKey("card_set.id"), nullable=False) diff --git a/python/kontor/gui/main_window.py b/python/kontor/gui/main_window.py index 6e775bc..eaba732 100644 --- a/python/kontor/gui/main_window.py +++ b/python/kontor/gui/main_window.py @@ -1,6 +1,10 @@ +from typing import Any + from PySide6.QtGui import QAction, QIcon from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView from PySide6.QtWidgets import QLabel, QMainWindow +from cement.core.config import ConfigHandler +from sqlalchemy import Engine from ..database import KontorDB from ..database.media import MediaFile @@ -12,7 +16,7 @@ from .table_model import KontorTableModel class MainWindow(QMainWindow): - def __init__(self, session, engine, log): + def __init__(self, engine: Engine, config: ConfigHandler, log): super().__init__() self.tick = QIcon('kontor/res/tick.png') @@ -30,7 +34,7 @@ class MainWindow(QMainWindow): self.data = [] self.filter = {} - self.kontor_db = KontorDB(session, engine, log) + self.kontor_db = KontorDB(engine, config, log) self.log = log self.central_widget = QWidget() parent_layout = QVBoxLayout() @@ -55,7 +59,9 @@ class MainWindow(QMainWindow): self.refreshAction = QAction(self.circle_icon, "&Refresh", self) self.refreshAction.triggered.connect(self.refresh) self.updateTitleAction = QAction("&Update Titles", self) + self.updateTitleAction.triggered.connect(self.update_title) self.downloadAction = QAction("&Download Videos", self) + self.downloadAction.triggered.connect(self.download_file) self.exitAction = QAction("&Beenden", self) self.exitAction.setShortcut("Alt+F4") self.exitAction.triggered.connect(self.close) @@ -118,6 +124,16 @@ class MainWindow(QMainWindow): else: self.statusBar.showMessage("Export cancelled", 3000) + def update_title(self): + self.log.info("update title for table MediaFile") + self.statusBar.showMessage("update title for table MediaFile", 3000) + self.kontor_db.update_title() + + def download_file(self): + self.log.info("download videos for table MediaFile") + self.statusBar.showMessage("download videos for table MediaFile", 3000) + self.kontor_db.download_file() + def refresh(self): self.data[self.tabs.currentIndex()].refresh() diff --git a/python/kontor/main.py b/python/kontor/main.py index 9460b56..c0a3631 100644 --- a/python/kontor/main.py +++ b/python/kontor/main.py @@ -10,17 +10,20 @@ from .controllers.clibase import CliBase from .controllers.database import Database # configuration defaults -CONFIG = init_defaults('kontor', 'mariadb','output.json', 'output.yaml') +CONFIG = init_defaults('kontor', 'mariadb', 'media') CONFIG['kontor']['foo'] = 'bar' CONFIG['mariadb']['user'] = 'kontor' CONFIG['mariadb']['password'] = 'kontor' CONFIG['mariadb']['host'] = '127.0.0.1' CONFIG['mariadb']['port'] = '3306' CONFIG['mariadb']['database'] = 'kontor' +CONFIG['media']['ytdlp'] = '/home/tpeetz/bin/yt-dlp' +CONFIG['media']['dir'] = '/data/media' META = init_defaults('output.json', 'output.yaml') META['output.json']['overridable'] = True META['output.yaml']['overridable'] = True + def extend_sqlalchemy(app): app.log.info('extending kontor application with sqlalchemy') connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format( @@ -34,15 +37,9 @@ def extend_sqlalchemy(app): engine = create_engine(connect_string) Base.metadata.create_all(bind=engine) __session__ = sessionmaker(bind=engine) - app.extend('session', __session__()) app.extend('engine', engine) -def close_session(app): - app.log.info('close session') - app.session.close() - - class Kontor(App): """Kontor primary application.""" @@ -79,7 +76,6 @@ class Kontor(App): hooks = [ ('post_setup', extend_sqlalchemy), - ('pre_close', close_session), ] # register handlers handlers = [ diff --git a/python/requirements.txt b/python/requirements.txt index 06a15f9..f6e0136 100644 --- a/python/requirements.txt +++ b/python/requirements.txt @@ -5,3 +5,4 @@ cement[colorlog] mariadb sqlalchemy PySide6 +beautifulsoup4 From 917e287784ee37223fd10674834baeb296353578 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Fri, 17 Jan 2025 13:38:16 +0100 Subject: [PATCH 52/91] fix missing argument for KontorDB in import_cmd and export --- python/kontor/controllers/database.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/kontor/controllers/database.py b/python/kontor/controllers/database.py index 04fe2fb..565cbd8 100644 --- a/python/kontor/controllers/database.py +++ b/python/kontor/controllers/database.py @@ -26,7 +26,7 @@ class Database(Controller): } if self.app.pargs.db_file is not None: data['db_file'] = self.app.pargs.db_file - kontor_db = KontorDB(self.app.engine, self.app.log) + kontor_db = KontorDB(self.app.engine, self.app.config, self.app.log) kontor_db.export_db(data['export_type'], data['db_file']) self.app.render(data, 'command1.jinja2') @@ -47,6 +47,6 @@ class Database(Controller): } if self.app.pargs.db_file is not None: data['db_file'] = self.app.pargs.db_file - kontor_db = KontorDB(self.app.engine, self.app.log) + kontor_db = KontorDB(self.app.engine, self.app.config, self.app.log) self.app.render(data, 'import.jinja2') kontor_db.import_db(data['db_file'], self.app.pargs.dry_run) From 3c4d3ad326ec92803d44a374dce7ae9fbea9f2d2 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Fri, 17 Jan 2025 17:19:31 +0100 Subject: [PATCH 53/91] add check files command --- python/kontor/controllers/media.py | 18 ++++++++++++++++++ python/kontor/database/__init__.py | 14 +++++++++++++- python/kontor/gui/main_window.py | 8 ++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/python/kontor/controllers/media.py b/python/kontor/controllers/media.py index e39c724..ecee44d 100644 --- a/python/kontor/controllers/media.py +++ b/python/kontor/controllers/media.py @@ -59,3 +59,21 @@ class Media(Controller): else: print("no url was given.") + + @ex( + help='check files if existing', + arguments=[ + (['-d', '--dir'], + {'help': 'directory to store videos', + 'action': 'store', + 'dest': 'media_dir'}) + ], + ) + def check(self): + data = { + 'media_dir': '/data/media', + } + if self.app.pargs.media_dir is not None: + data['media_dir'] = self.app.pargs.media_dir + kontor_db = KontorDB(self.app.engine, self.app.config, self.app.log) + kontor_db.check_files() diff --git a/python/kontor/database/__init__.py b/python/kontor/database/__init__.py index 0c524fa..031799b 100644 --- a/python/kontor/database/__init__.py +++ b/python/kontor/database/__init__.py @@ -322,8 +322,14 @@ class KontorDB: filename = self.download_url(link) if filename is None: link.file_name = filename + link.should_download = 1 + else: + download_file = Path(filename) + download_file.with_name(f"{link.id}{download_file.suffix}") + link.file_name = download_file.name link.should_download = 0 - session.commit() + link.cloud_link = download_file.absolute() + session.commit() def parse_output(self, lines_list): file_name = "" @@ -354,3 +360,9 @@ class KontorDB: return self.parse_output(lines_list) else: return None + + def check_files(self): + media_dir = Path(self.config.get('media', 'dir')) + if not media_dir.exists(): + return + self.log.info(f"check files in {media_dir}") diff --git a/python/kontor/gui/main_window.py b/python/kontor/gui/main_window.py index eaba732..a835b89 100644 --- a/python/kontor/gui/main_window.py +++ b/python/kontor/gui/main_window.py @@ -62,6 +62,8 @@ class MainWindow(QMainWindow): self.updateTitleAction.triggered.connect(self.update_title) self.downloadAction = QAction("&Download Videos", self) self.downloadAction.triggered.connect(self.download_file) + self.checkFileAction = QAction("&Check files", self) + self.checkFileAction.triggered.connect(self.check_files) self.exitAction = QAction("&Beenden", self) self.exitAction.setShortcut("Alt+F4") self.exitAction.triggered.connect(self.close) @@ -82,6 +84,7 @@ class MainWindow(QMainWindow): media_file_menu = QMenu("&MediaFile") media_file_menu.addAction(self.updateTitleAction) media_file_menu.addAction(self.downloadAction) + media_file_menu.addAction(self.checkFileAction) kontor_menu.addMenu(comic_menu) kontor_menu.addMenu(tysc_menu) kontor_menu.addMenu(media_file_menu) @@ -134,6 +137,11 @@ class MainWindow(QMainWindow): self.statusBar.showMessage("download videos for table MediaFile", 3000) self.kontor_db.download_file() + def check_files(self): + self.log.info("check files") + self.statusBar.showMessage("check files for table MediaFile", 3000) + self.kontor_db.check_files() + def refresh(self): self.data[self.tabs.currentIndex()].refresh() From f3c59c11ba3671a9b32b518a89fa6248cb341658 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sat, 18 Jan 2025 01:28:27 +0100 Subject: [PATCH 54/91] add progress in statusbar --- python/kontor/gui/main_window.py | 14 ++++++++++++-- python/kontor/gui/progress.py | 18 ++++++++++++++++++ python/kontor/main.py | 2 +- 3 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 python/kontor/gui/progress.py diff --git a/python/kontor/gui/main_window.py b/python/kontor/gui/main_window.py index a835b89..64c0d74 100644 --- a/python/kontor/gui/main_window.py +++ b/python/kontor/gui/main_window.py @@ -1,11 +1,12 @@ from typing import Any from PySide6.QtGui import QAction, QIcon -from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView +from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView, QProgressBar from PySide6.QtWidgets import QLabel, QMainWindow from cement.core.config import ConfigHandler from sqlalchemy import Engine +from .progress import ProgressUpdate from ..database import KontorDB from ..database.media import MediaFile from ..database.comic import Comic @@ -30,6 +31,8 @@ class MainWindow(QMainWindow): self._create_actions() self._create_menubar() self._create_toolbars() + self.status_progress = QProgressBar() + self.progress_update = ProgressUpdate(self.status_progress) self._create_statusbar() self.data = [] @@ -104,7 +107,8 @@ class MainWindow(QMainWindow): self.statusBar = self.statusBar() self.statusBar.showMessage("Kontor ready", 6000) self.status_label = QLabel("") - self.statusBar.addPermanentWidget(self.status_label) + self.status_progress.setEnabled(False) + self.statusBar.addPermanentWidget(self.status_progress) def about(self): QMessageBox.about(self.central_widget, "Über Kontor", f"Python: 3.11\nKontor: 0.1.0") @@ -130,12 +134,18 @@ class MainWindow(QMainWindow): def update_title(self): self.log.info("update title for table MediaFile") self.statusBar.showMessage("update title for table MediaFile", 3000) + self.status_progress.setEnabled(True) self.kontor_db.update_title() + self.status_progress.setEnabled(False) + self.refresh() def download_file(self): self.log.info("download videos for table MediaFile") self.statusBar.showMessage("download videos for table MediaFile", 3000) + self.status_progress.setEnabled(True) self.kontor_db.download_file() + self.status_progress.setEnabled(False) + self.refresh() def check_files(self): self.log.info("check files") diff --git a/python/kontor/gui/progress.py b/python/kontor/gui/progress.py new file mode 100644 index 0000000..1b44c26 --- /dev/null +++ b/python/kontor/gui/progress.py @@ -0,0 +1,18 @@ +from PySide6.QtWidgets import QProgressBar + + +class ProgressUpdate: + def __init__(self, progress: QProgressBar): + self.start = 0 + self.end = 0 + self.current = 0 + self.progress = progress + + def start(self, start_value, end_value): + self.start = start_value + self.end = end_value + self.current = start_value + self.progress.update() + + def update(self, current): + self.progress.update() diff --git a/python/kontor/main.py b/python/kontor/main.py index c0a3631..3375319 100644 --- a/python/kontor/main.py +++ b/python/kontor/main.py @@ -17,7 +17,7 @@ CONFIG['mariadb']['password'] = 'kontor' CONFIG['mariadb']['host'] = '127.0.0.1' CONFIG['mariadb']['port'] = '3306' CONFIG['mariadb']['database'] = 'kontor' -CONFIG['media']['ytdlp'] = '/home/tpeetz/bin/yt-dlp' +CONFIG['media']['yt-dlp'] = '/home/tpeetz/bin/yt-dlp' CONFIG['media']['dir'] = '/data/media' META = init_defaults('output.json', 'output.yaml') META['output.json']['overridable'] = True From f07c7b74eef19eb5d684ee74a4b6cba96f3d5c6e Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 19 Jan 2025 17:49:13 +0100 Subject: [PATCH 55/91] prepare progress bar for download --- python/kontor/database/__init__.py | 3 ++- python/kontor/gui/main_window.py | 2 +- python/kontor/main.py | 8 ++++---- python/requirements.txt | 1 + 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/python/kontor/database/__init__.py b/python/kontor/database/__init__.py index 031799b..54c296b 100644 --- a/python/kontor/database/__init__.py +++ b/python/kontor/database/__init__.py @@ -19,6 +19,7 @@ from .comic import Comic, Artist, Publisher, Issue, StoryArc, TradePaperback, Vo from .metadata import MetaDataTable, MetaDataColumn from .tysc import Card, CardSet, Sport, Team, FieldPosition, Rooster, Player, Vendor from .media import MediaFile, MediaArticle, MediaVideo +from ..gui.progress import ProgressUpdate class KontorDB: @@ -305,7 +306,7 @@ class KontorDB: link.review = 0 session.commit() - def download_file(self, dry_run=False): + def download_file(self, dry_run=False, update: ProgressUpdate=None): self.log.info(f"download marked files of media_file") __session__ = sessionmaker(self.engine) with __session__() as session: diff --git a/python/kontor/gui/main_window.py b/python/kontor/gui/main_window.py index 64c0d74..0996d02 100644 --- a/python/kontor/gui/main_window.py +++ b/python/kontor/gui/main_window.py @@ -143,7 +143,7 @@ class MainWindow(QMainWindow): self.log.info("download videos for table MediaFile") self.statusBar.showMessage("download videos for table MediaFile", 3000) self.status_progress.setEnabled(True) - self.kontor_db.download_file() + self.kontor_db.download_file(False, self.progress_update) self.status_progress.setEnabled(False) self.refresh() diff --git a/python/kontor/main.py b/python/kontor/main.py index 3375319..2f2e469 100644 --- a/python/kontor/main.py +++ b/python/kontor/main.py @@ -4,10 +4,10 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from kontor.controllers.media import Media -from .core.exc import KontorError -from .database.base import Base -from .controllers.clibase import CliBase -from .controllers.database import Database +from kontor.core.exc import KontorError +from kontor.database.base import Base +from kontor.controllers.clibase import CliBase +from kontor.controllers.database import Database # configuration defaults CONFIG = init_defaults('kontor', 'mariadb', 'media') diff --git a/python/requirements.txt b/python/requirements.txt index f6e0136..76bba2b 100644 --- a/python/requirements.txt +++ b/python/requirements.txt @@ -6,3 +6,4 @@ mariadb sqlalchemy PySide6 beautifulsoup4 + From ada723dc48e7d967f8d9ef95dd2ab8f1995101e7 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 19 Jan 2025 23:36:52 +0100 Subject: [PATCH 56/91] separate cli and gui application in own python packages. provide database schema as python package. --- python/kontor-cli.backup/.gitignore | 105 +++++++++ python/{ => kontor-cli.backup}/CHANGELOG.md | 2 +- python/{ => kontor-cli.backup}/Dockerfile | 6 +- python/{ => kontor-cli.backup}/LICENSE.md | 0 python/{ => kontor-cli.backup}/MANIFEST.in | 0 python/{ => kontor-cli.backup}/Makefile | 0 python/{ => kontor-cli.backup}/README.md | 4 +- .../config/kontor.yml.example | 2 +- python/{ => kontor-cli.backup}/docs/.gitkeep | 0 .../kontor/__init__.py | 0 .../kontor/controllers/__init__.py | 0 .../kontor/controllers/base.py} | 18 +- .../kontor/controllers/database.py | 3 +- .../kontor/controllers/media.py | 0 .../kontor/core/__init__.py | 0 python/kontor-cli.backup/kontor/core/exc.py | 4 + .../kontor-cli.backup/kontor/core/version.py | 7 + .../kontor/ext/__init__.py | 0 python/{ => kontor-cli.backup}/kontor/main.py | 27 +-- .../kontor/plugins}/__init__.py | 0 .../kontor/templates}/__init__.py | 0 .../kontor/templates/command1.jinja2 | 0 .../kontor/templates/download.jinja2 | 0 .../kontor/templates/import.jinja2 | 0 .../kontor/templates/update.jinja2 | 0 .../requirements-dev.txt | 0 python/kontor-cli.backup/requirements.txt | 6 + python/{ => kontor-cli.backup}/setup.cfg | 0 python/{ => kontor-cli.backup}/setup.py | 6 +- python/kontor-cli.backup/tests/conftest.py | 16 ++ python/kontor-cli.backup/tests/test_kontor.py | 36 +++ python/kontor-cli/.gitignore | 105 +++++++++ python/kontor-cli/CHANGELOG.md | 5 + python/kontor-cli/Dockerfile | 10 + python/kontor-cli/LICENSE.md | 1 + python/kontor-cli/MANIFEST.in | 5 + python/kontor-cli/Makefile | 31 +++ python/kontor-cli/README.md | 69 ++++++ python/kontor-cli/config/kontor.yml.example | 46 ++++ .../__init__.py => kontor-cli/docs/.gitkeep} | 0 .../kontor-cli/kontor}/__init__.py | 0 .../kontor-cli/kontor/controllers/__init__.py | 0 python/kontor-cli/kontor/controllers/base.py | 60 +++++ .../kontor-cli/kontor/controllers/database.py | 51 +++++ python/kontor-cli/kontor/controllers/media.py | 81 +++++++ python/kontor-cli/kontor/core/__init__.py | 0 python/{ => kontor-cli}/kontor/core/exc.py | 0 .../{ => kontor-cli}/kontor/core/version.py | 0 python/kontor-cli/kontor/ext/__init__.py | 0 python/kontor-cli/kontor/main.py | 118 ++++++++++ python/kontor-cli/kontor/plugins/__init__.py | 0 .../kontor-cli/kontor/templates/__init__.py | 0 .../kontor/templates/command1.jinja2 | 4 + python/kontor-cli/requirements-dev.txt | 8 + python/{ => kontor-cli}/requirements.txt | 2 - python/kontor-cli/setup.cfg | 0 python/kontor-cli/setup.py | 28 +++ python/kontor-cli/tests/conftest.py | 16 ++ python/kontor-cli/tests/test_kontor.py | 36 +++ python/kontor-gui/.gitignore | 7 + python/kontor-gui/gui/__init__.py | 0 .../{kontor => kontor-gui}/gui/data_view.py | 0 .../gui/data_view_model.py | 0 python/{kontor => kontor-gui}/gui/dialogs.py | 0 .../{kontor => kontor-gui}/gui/main_window.py | 30 ++- .../gui/model_config.py | 9 +- python/{kontor => kontor-gui}/gui/progress.py | 0 .../{kontor => kontor-gui}/gui/table_model.py | 0 python/kontor-gui/kontor.py | 43 ++++ python/kontor-gui/pyvenv.cfg | 5 + python/kontor-gui/requirements.txt | 6 + .../res/application-export.png | Bin .../res/application-import.png | Bin .../res/arrow-circle-double.png | Bin python/{kontor => kontor-gui}/res/cross.png | Bin python/{kontor => kontor-gui}/res/tick.png | Bin python/kontor-schema/README.md | 4 + .../site/python3.11/greenlet/greenlet.h | 164 ++++++++++++++ .../kontor_schema}/__init__.py | 31 ++- .../kontor_schema}/base.py | 0 .../kontor_schema}/bookshelf.py | 0 .../kontor_schema}/comic.py | 0 .../kontor_schema}/media.py | 0 .../kontor_schema}/metadata.py | 0 .../kontor_schema}/tysc.py | 0 python/kontor-schema/pyvenv.cfg | 5 + python/kontor-schema/requirements.txt | 2 + python/kontor-schema/setup.py | 23 ++ python/kontor-video/README.md | 3 + python/kontor-video/kontor_video/__init__.py | 21 ++ python/kontor-video/pyvenv.cfg | 5 + python/kontor-video/requirements.txt | 2 + python/kontor-video/setup.py | 23 ++ qt/.gitignore | 2 - qt/database/__init__.py | 212 ------------------ qt/database/base.py | 18 -- qt/database/comic.py | 130 ----------- qt/database/media.py | 25 --- qt/database/metadata.py | 49 ---- qt/database/tysc.py | 131 ----------- qt/gui/data_view.py | 12 - qt/gui/data_view_model.py | 32 --- qt/gui/dialogs.py | 106 --------- qt/gui/main_window.py | 137 ----------- qt/gui/model_config.py | 65 ------ qt/gui/table_model.py | 108 --------- qt/kontor.py | 21 -- qt/pysidedeploy.spec | 98 -------- qt/res/application-export.png | Bin 513 -> 0 bytes qt/res/application-import.png | Bin 524 -> 0 bytes qt/res/arrow-circle-double.png | Bin 836 -> 0 bytes qt/res/cross.png | Bin 544 -> 0 bytes qt/res/tick.png | Bin 634 -> 0 bytes 113 files changed, 1224 insertions(+), 1223 deletions(-) create mode 100644 python/kontor-cli.backup/.gitignore rename python/{ => kontor-cli.backup}/CHANGELOG.md (50%) rename python/{ => kontor-cli.backup}/Dockerfile (58%) rename python/{ => kontor-cli.backup}/LICENSE.md (100%) rename python/{ => kontor-cli.backup}/MANIFEST.in (100%) rename python/{ => kontor-cli.backup}/Makefile (100%) rename python/{ => kontor-cli.backup}/README.md (97%) rename python/{ => kontor-cli.backup}/config/kontor.yml.example (96%) rename python/{ => kontor-cli.backup}/docs/.gitkeep (100%) rename python/{ => kontor-cli.backup}/kontor/__init__.py (100%) rename python/{ => kontor-cli.backup}/kontor/controllers/__init__.py (100%) rename python/{kontor/controllers/clibase.py => kontor-cli.backup/kontor/controllers/base.py} (66%) rename python/{ => kontor-cli.backup}/kontor/controllers/database.py (94%) rename python/{ => kontor-cli.backup}/kontor/controllers/media.py (100%) rename python/{ => kontor-cli.backup}/kontor/core/__init__.py (100%) create mode 100644 python/kontor-cli.backup/kontor/core/exc.py create mode 100644 python/kontor-cli.backup/kontor/core/version.py rename python/{ => kontor-cli.backup}/kontor/ext/__init__.py (100%) rename python/{ => kontor-cli.backup}/kontor/main.py (84%) rename python/{kontor/gui => kontor-cli.backup/kontor/plugins}/__init__.py (100%) rename python/{kontor/plugins => kontor-cli.backup/kontor/templates}/__init__.py (100%) rename python/{ => kontor-cli.backup}/kontor/templates/command1.jinja2 (100%) rename python/{ => kontor-cli.backup}/kontor/templates/download.jinja2 (100%) rename python/{ => kontor-cli.backup}/kontor/templates/import.jinja2 (100%) rename python/{ => kontor-cli.backup}/kontor/templates/update.jinja2 (100%) rename python/{ => kontor-cli.backup}/requirements-dev.txt (100%) create mode 100644 python/kontor-cli.backup/requirements.txt rename python/{ => kontor-cli.backup}/setup.cfg (100%) rename python/{ => kontor-cli.backup}/setup.py (81%) create mode 100644 python/kontor-cli.backup/tests/conftest.py create mode 100644 python/kontor-cli.backup/tests/test_kontor.py create mode 100644 python/kontor-cli/.gitignore create mode 100644 python/kontor-cli/CHANGELOG.md create mode 100644 python/kontor-cli/Dockerfile create mode 100644 python/kontor-cli/LICENSE.md create mode 100644 python/kontor-cli/MANIFEST.in create mode 100644 python/kontor-cli/Makefile create mode 100644 python/kontor-cli/README.md create mode 100644 python/kontor-cli/config/kontor.yml.example rename python/{kontor/templates/__init__.py => kontor-cli/docs/.gitkeep} (100%) rename {qt/gui => python/kontor-cli/kontor}/__init__.py (100%) create mode 100644 python/kontor-cli/kontor/controllers/__init__.py create mode 100644 python/kontor-cli/kontor/controllers/base.py create mode 100644 python/kontor-cli/kontor/controllers/database.py create mode 100644 python/kontor-cli/kontor/controllers/media.py create mode 100644 python/kontor-cli/kontor/core/__init__.py rename python/{ => kontor-cli}/kontor/core/exc.py (100%) rename python/{ => kontor-cli}/kontor/core/version.py (100%) create mode 100644 python/kontor-cli/kontor/ext/__init__.py create mode 100644 python/kontor-cli/kontor/main.py create mode 100644 python/kontor-cli/kontor/plugins/__init__.py create mode 100644 python/kontor-cli/kontor/templates/__init__.py create mode 100644 python/kontor-cli/kontor/templates/command1.jinja2 create mode 100644 python/kontor-cli/requirements-dev.txt rename python/{ => kontor-cli}/requirements.txt (77%) create mode 100644 python/kontor-cli/setup.cfg create mode 100644 python/kontor-cli/setup.py create mode 100644 python/kontor-cli/tests/conftest.py create mode 100644 python/kontor-cli/tests/test_kontor.py create mode 100644 python/kontor-gui/.gitignore create mode 100644 python/kontor-gui/gui/__init__.py rename python/{kontor => kontor-gui}/gui/data_view.py (100%) rename python/{kontor => kontor-gui}/gui/data_view_model.py (100%) rename python/{kontor => kontor-gui}/gui/dialogs.py (100%) rename python/{kontor => kontor-gui}/gui/main_window.py (88%) rename python/{kontor => kontor-gui}/gui/model_config.py (90%) rename python/{kontor => kontor-gui}/gui/progress.py (100%) rename python/{kontor => kontor-gui}/gui/table_model.py (100%) create mode 100644 python/kontor-gui/kontor.py create mode 100644 python/kontor-gui/pyvenv.cfg create mode 100644 python/kontor-gui/requirements.txt rename python/{kontor => kontor-gui}/res/application-export.png (100%) rename python/{kontor => kontor-gui}/res/application-import.png (100%) rename python/{kontor => kontor-gui}/res/arrow-circle-double.png (100%) rename python/{kontor => kontor-gui}/res/cross.png (100%) rename python/{kontor => kontor-gui}/res/tick.png (100%) create mode 100644 python/kontor-schema/README.md create mode 100644 python/kontor-schema/include/site/python3.11/greenlet/greenlet.h rename python/{kontor/database => kontor-schema/kontor_schema}/__init__.py (94%) rename python/{kontor/database => kontor-schema/kontor_schema}/base.py (100%) rename python/{kontor/database => kontor-schema/kontor_schema}/bookshelf.py (100%) rename python/{kontor/database => kontor-schema/kontor_schema}/comic.py (100%) rename python/{kontor/database => kontor-schema/kontor_schema}/media.py (100%) rename python/{kontor/database => kontor-schema/kontor_schema}/metadata.py (100%) rename python/{kontor/database => kontor-schema/kontor_schema}/tysc.py (100%) create mode 100644 python/kontor-schema/pyvenv.cfg create mode 100644 python/kontor-schema/requirements.txt create mode 100644 python/kontor-schema/setup.py create mode 100644 python/kontor-video/README.md create mode 100644 python/kontor-video/kontor_video/__init__.py create mode 100644 python/kontor-video/pyvenv.cfg create mode 100644 python/kontor-video/requirements.txt create mode 100644 python/kontor-video/setup.py delete mode 100644 qt/.gitignore delete mode 100644 qt/database/__init__.py delete mode 100644 qt/database/base.py delete mode 100644 qt/database/comic.py delete mode 100644 qt/database/media.py delete mode 100644 qt/database/metadata.py delete mode 100644 qt/database/tysc.py delete mode 100644 qt/gui/data_view.py delete mode 100644 qt/gui/data_view_model.py delete mode 100644 qt/gui/dialogs.py delete mode 100644 qt/gui/main_window.py delete mode 100644 qt/gui/model_config.py delete mode 100644 qt/gui/table_model.py delete mode 100644 qt/kontor.py delete mode 100644 qt/pysidedeploy.spec delete mode 100644 qt/res/application-export.png delete mode 100644 qt/res/application-import.png delete mode 100644 qt/res/arrow-circle-double.png delete mode 100644 qt/res/cross.png delete mode 100644 qt/res/tick.png diff --git a/python/kontor-cli.backup/.gitignore b/python/kontor-cli.backup/.gitignore new file mode 100644 index 0000000..a74b246 --- /dev/null +++ b/python/kontor-cli.backup/.gitignore @@ -0,0 +1,105 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +coverage-report/ +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/python/CHANGELOG.md b/python/kontor-cli.backup/CHANGELOG.md similarity index 50% rename from python/CHANGELOG.md rename to python/kontor-cli.backup/CHANGELOG.md index 6c95d97..10e1f8a 100644 --- a/python/CHANGELOG.md +++ b/python/kontor-cli.backup/CHANGELOG.md @@ -1,4 +1,4 @@ -# Kontor Change History +# Kontor CLI Change History ## 0.0.1 diff --git a/python/Dockerfile b/python/kontor-cli.backup/Dockerfile similarity index 58% rename from python/Dockerfile rename to python/kontor-cli.backup/Dockerfile index a3e005d..d32cf5c 100644 --- a/python/Dockerfile +++ b/python/kontor-cli.backup/Dockerfile @@ -1,10 +1,6 @@ -#FROM python:3.9-alpine -FROM python:3.11-bookworm +FROM python:3.9-alpine LABEL MAINTAINER="Thomas Peetz " ENV PS1="\[\e[0;33m\]|> kontor <| \[\e[1;35m\]\W\[\e[0m\] \[\e[0m\]# " -ENV DEBIAN_FRONTEND noninteractive -RUN apt-get update && apt-get install -y apt-utils libmariadb3 libmariadb-dev -RUN rm -rf /var/lib/apt/lists/* WORKDIR /src COPY . /src diff --git a/python/LICENSE.md b/python/kontor-cli.backup/LICENSE.md similarity index 100% rename from python/LICENSE.md rename to python/kontor-cli.backup/LICENSE.md diff --git a/python/MANIFEST.in b/python/kontor-cli.backup/MANIFEST.in similarity index 100% rename from python/MANIFEST.in rename to python/kontor-cli.backup/MANIFEST.in diff --git a/python/Makefile b/python/kontor-cli.backup/Makefile similarity index 100% rename from python/Makefile rename to python/kontor-cli.backup/Makefile diff --git a/python/README.md b/python/kontor-cli.backup/README.md similarity index 97% rename from python/README.md rename to python/kontor-cli.backup/README.md index 6afe593..a95f4ea 100644 --- a/python/README.md +++ b/python/kontor-cli.backup/README.md @@ -1,4 +1,4 @@ -# Kontor CLI Tool +# Kontor CLI ## Installation @@ -59,7 +59,7 @@ $ make dist-upload ### Docker -Included is a basic `Dockerfile` for building and distributing `Kontor`, +Included is a basic `Dockerfile` for building and distributing `Kontor CLI`, and can be built with the included `make` helper: ``` diff --git a/python/config/kontor.yml.example b/python/kontor-cli.backup/config/kontor.yml.example similarity index 96% rename from python/config/kontor.yml.example rename to python/kontor-cli.backup/config/kontor.yml.example index ba6507c..6806151 100644 --- a/python/config/kontor.yml.example +++ b/python/kontor-cli.backup/config/kontor.yml.example @@ -1,4 +1,4 @@ -### Kontor Configuration Settings +### Kontor CLI Configuration Settings --- kontor: diff --git a/python/docs/.gitkeep b/python/kontor-cli.backup/docs/.gitkeep similarity index 100% rename from python/docs/.gitkeep rename to python/kontor-cli.backup/docs/.gitkeep diff --git a/python/kontor/__init__.py b/python/kontor-cli.backup/kontor/__init__.py similarity index 100% rename from python/kontor/__init__.py rename to python/kontor-cli.backup/kontor/__init__.py diff --git a/python/kontor/controllers/__init__.py b/python/kontor-cli.backup/kontor/controllers/__init__.py similarity index 100% rename from python/kontor/controllers/__init__.py rename to python/kontor-cli.backup/kontor/controllers/__init__.py diff --git a/python/kontor/controllers/clibase.py b/python/kontor-cli.backup/kontor/controllers/base.py similarity index 66% rename from python/kontor/controllers/clibase.py rename to python/kontor-cli.backup/kontor/controllers/base.py index 0491128..90153a3 100644 --- a/python/kontor/controllers/clibase.py +++ b/python/kontor-cli.backup/kontor/controllers/base.py @@ -1,16 +1,15 @@ -from PySide6.QtWidgets import QApplication + from cement import Controller, ex from cement.utils.version import get_version_banner from ..core.version import get_version -from ..gui.main_window import MainWindow VERSION_BANNER = """ -Kontor CLI Tool %s +Kontor CLI %s %s """ % (get_version(), get_version_banner()) -class CliBase(Controller): +class Base(Controller): class Meta: label = 'base' @@ -18,7 +17,7 @@ class CliBase(Controller): description = 'Kontor CLI Tool' # text displayed at the bottom of --help output - epilog = 'Usage: kontor (gui | database | media) [subcommands]' + epilog = 'Usage: kontor (database | media) [subcommands]' # controller level arguments. ex: 'kontor --version' arguments = [ @@ -34,12 +33,3 @@ class CliBase(Controller): def _default(self): """Default action if no sub-command is passed.""" self.app.args.print_help() - - @ex( - help='start GUI' - ) - def gui(self): - application = QApplication([]) - window = MainWindow(self.app.engine, self.app.config, self.app.log) - window.show() - application.exec() diff --git a/python/kontor/controllers/database.py b/python/kontor-cli.backup/kontor/controllers/database.py similarity index 94% rename from python/kontor/controllers/database.py rename to python/kontor-cli.backup/kontor/controllers/database.py index 565cbd8..5917d0a 100644 --- a/python/kontor/controllers/database.py +++ b/python/kontor-cli.backup/kontor/controllers/database.py @@ -1,6 +1,5 @@ from cement import Controller, ex - -from ..database import KontorDB +from kontor_schema import KontorDB class Database(Controller): diff --git a/python/kontor/controllers/media.py b/python/kontor-cli.backup/kontor/controllers/media.py similarity index 100% rename from python/kontor/controllers/media.py rename to python/kontor-cli.backup/kontor/controllers/media.py diff --git a/python/kontor/core/__init__.py b/python/kontor-cli.backup/kontor/core/__init__.py similarity index 100% rename from python/kontor/core/__init__.py rename to python/kontor-cli.backup/kontor/core/__init__.py diff --git a/python/kontor-cli.backup/kontor/core/exc.py b/python/kontor-cli.backup/kontor/core/exc.py new file mode 100644 index 0000000..8dd63e8 --- /dev/null +++ b/python/kontor-cli.backup/kontor/core/exc.py @@ -0,0 +1,4 @@ + +class KontorCliError(Exception): + """Generic errors.""" + pass diff --git a/python/kontor-cli.backup/kontor/core/version.py b/python/kontor-cli.backup/kontor/core/version.py new file mode 100644 index 0000000..846814d --- /dev/null +++ b/python/kontor-cli.backup/kontor/core/version.py @@ -0,0 +1,7 @@ + +from cement.utils.version import get_version as cement_get_version + +VERSION = (0, 1, 0, 'alpha', 0) + +def get_version(version=VERSION): + return cement_get_version(version) diff --git a/python/kontor/ext/__init__.py b/python/kontor-cli.backup/kontor/ext/__init__.py similarity index 100% rename from python/kontor/ext/__init__.py rename to python/kontor-cli.backup/kontor/ext/__init__.py diff --git a/python/kontor/main.py b/python/kontor-cli.backup/kontor/main.py similarity index 84% rename from python/kontor/main.py rename to python/kontor-cli.backup/kontor/main.py index 2f2e469..c8084b8 100644 --- a/python/kontor/main.py +++ b/python/kontor-cli.backup/kontor/main.py @@ -1,13 +1,13 @@ + from cement import App, TestApp, init_defaults from cement.core.exc import CaughtSignal from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker -from kontor.controllers.media import Media -from kontor.core.exc import KontorError -from kontor.database.base import Base -from kontor.controllers.clibase import CliBase -from kontor.controllers.database import Database +from .controllers.database import Database +from .controllers.media import Media +from .core.exc import KontorCliError +from .controllers.base import Base # configuration defaults CONFIG = init_defaults('kontor', 'mariadb', 'media') @@ -40,8 +40,8 @@ def extend_sqlalchemy(app): app.extend('engine', engine) -class Kontor(App): - """Kontor primary application.""" +class KontorCli(App): + """Kontor CLI primary application.""" class Meta: label = 'kontor' @@ -77,23 +77,24 @@ class Kontor(App): hooks = [ ('post_setup', extend_sqlalchemy), ] + # register handlers handlers = [ - CliBase, + Base, Database, Media, ] -class KontorTest(TestApp, Kontor): - """A sub-class of Kontor that is better suited for testing.""" +class KontorCliTest(TestApp,KontorCli): + """A sub-class of KontorCli that is better suited for testing.""" class Meta: label = 'kontor' def main(): - with Kontor() as app: + with KontorCli() as app: try: app.run() @@ -105,8 +106,8 @@ def main(): import traceback traceback.print_exc() - except KontorError as e: - print('KontorError > %s' % e.args[0]) + except KontorCliError as e: + print('KontorCliError > %s' % e.args[0]) app.exit_code = 1 if app.debug is True: diff --git a/python/kontor/gui/__init__.py b/python/kontor-cli.backup/kontor/plugins/__init__.py similarity index 100% rename from python/kontor/gui/__init__.py rename to python/kontor-cli.backup/kontor/plugins/__init__.py diff --git a/python/kontor/plugins/__init__.py b/python/kontor-cli.backup/kontor/templates/__init__.py similarity index 100% rename from python/kontor/plugins/__init__.py rename to python/kontor-cli.backup/kontor/templates/__init__.py diff --git a/python/kontor/templates/command1.jinja2 b/python/kontor-cli.backup/kontor/templates/command1.jinja2 similarity index 100% rename from python/kontor/templates/command1.jinja2 rename to python/kontor-cli.backup/kontor/templates/command1.jinja2 diff --git a/python/kontor/templates/download.jinja2 b/python/kontor-cli.backup/kontor/templates/download.jinja2 similarity index 100% rename from python/kontor/templates/download.jinja2 rename to python/kontor-cli.backup/kontor/templates/download.jinja2 diff --git a/python/kontor/templates/import.jinja2 b/python/kontor-cli.backup/kontor/templates/import.jinja2 similarity index 100% rename from python/kontor/templates/import.jinja2 rename to python/kontor-cli.backup/kontor/templates/import.jinja2 diff --git a/python/kontor/templates/update.jinja2 b/python/kontor-cli.backup/kontor/templates/update.jinja2 similarity index 100% rename from python/kontor/templates/update.jinja2 rename to python/kontor-cli.backup/kontor/templates/update.jinja2 diff --git a/python/requirements-dev.txt b/python/kontor-cli.backup/requirements-dev.txt similarity index 100% rename from python/requirements-dev.txt rename to python/kontor-cli.backup/requirements-dev.txt diff --git a/python/kontor-cli.backup/requirements.txt b/python/kontor-cli.backup/requirements.txt new file mode 100644 index 0000000..bee2df1 --- /dev/null +++ b/python/kontor-cli.backup/requirements.txt @@ -0,0 +1,6 @@ +cement==3.0.12 +cement[jinja2] +cement[yaml] +cement[colorlog] +mariadb +sqlalchemy diff --git a/python/setup.cfg b/python/kontor-cli.backup/setup.cfg similarity index 100% rename from python/setup.cfg rename to python/kontor-cli.backup/setup.cfg diff --git a/python/setup.py b/python/kontor-cli.backup/setup.py similarity index 81% rename from python/setup.py rename to python/kontor-cli.backup/setup.py index 2f6b226..185d9fa 100644 --- a/python/setup.py +++ b/python/kontor-cli.backup/setup.py @@ -11,15 +11,15 @@ f.close() setup( name='kontor', version=VERSION, - description='Kontor CLI Tool', + description='Kontor CLI', long_description=LONG_DESCRIPTION, long_description_content_type='text/markdown', author='Thomas Peetz', author_email='thomas.peetz@thpeetz.de', - url='https://gitlab.com/tpeetz/kontor', + url='https://gitlab.com/tpeetz/kontor/', license='MIT', packages=find_packages(exclude=['ez_setup', 'tests*']), - package_data={'kontor': ['templates/*', 'res/*',]}, + package_data={'kontor': ['templates/*']}, include_package_data=True, entry_points=""" [console_scripts] diff --git a/python/kontor-cli.backup/tests/conftest.py b/python/kontor-cli.backup/tests/conftest.py new file mode 100644 index 0000000..5124e2e --- /dev/null +++ b/python/kontor-cli.backup/tests/conftest.py @@ -0,0 +1,16 @@ +""" +PyTest Fixtures. +""" + +import pytest +from cement import fs + +@pytest.fixture(scope="function") +def tmp(request): + """ + Create a `tmp` object that geneates a unique temporary directory, and file + for each test function that requires it. + """ + t = fs.Tmp() + yield t + t.remove() diff --git a/python/kontor-cli.backup/tests/test_kontor.py b/python/kontor-cli.backup/tests/test_kontor.py new file mode 100644 index 0000000..e93dd44 --- /dev/null +++ b/python/kontor-cli.backup/tests/test_kontor.py @@ -0,0 +1,36 @@ + +from pytest import raises +from kontor.main import KontorCliTest + +def test_kontor(): + # test kontor without any subcommands or arguments + with KontorCliTest() as app: + app.run() + assert app.exit_code == 0 + + +def test_kontor_debug(): + # test that debug mode is functional + argv = ['--debug'] + with KontorCliTest(argv=argv) as app: + app.run() + assert app.debug is True + + +def test_command1(): + # test command1 without arguments + argv = ['command1'] + with KontorCliTest(argv=argv) as app: + app.run() + data,output = app.last_rendered + assert data['foo'] == 'bar' + assert output.find('Foo => bar') + + + # test command1 with arguments + argv = ['command1', '--foo', 'not-bar'] + with KontorCliTest(argv=argv) as app: + app.run() + data,output = app.last_rendered + assert data['foo'] == 'not-bar' + assert output.find('Foo => not-bar') diff --git a/python/kontor-cli/.gitignore b/python/kontor-cli/.gitignore new file mode 100644 index 0000000..a74b246 --- /dev/null +++ b/python/kontor-cli/.gitignore @@ -0,0 +1,105 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +coverage-report/ +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/python/kontor-cli/CHANGELOG.md b/python/kontor-cli/CHANGELOG.md new file mode 100644 index 0000000..10e1f8a --- /dev/null +++ b/python/kontor-cli/CHANGELOG.md @@ -0,0 +1,5 @@ +# Kontor CLI Change History + +## 0.0.1 + +Initial release. diff --git a/python/kontor-cli/Dockerfile b/python/kontor-cli/Dockerfile new file mode 100644 index 0000000..d32cf5c --- /dev/null +++ b/python/kontor-cli/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.9-alpine +LABEL MAINTAINER="Thomas Peetz " +ENV PS1="\[\e[0;33m\]|> kontor <| \[\e[1;35m\]\W\[\e[0m\] \[\e[0m\]# " + +WORKDIR /src +COPY . /src +RUN pip install --no-cache-dir -r requirements.txt \ + && python setup.py install +WORKDIR / +ENTRYPOINT ["kontor"] diff --git a/python/kontor-cli/LICENSE.md b/python/kontor-cli/LICENSE.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/python/kontor-cli/LICENSE.md @@ -0,0 +1 @@ + diff --git a/python/kontor-cli/MANIFEST.in b/python/kontor-cli/MANIFEST.in new file mode 100644 index 0000000..1160952 --- /dev/null +++ b/python/kontor-cli/MANIFEST.in @@ -0,0 +1,5 @@ +recursive-include *.py +include setup.cfg +include README.md CHANGELOG.md LICENSE.md +include *.txt +recursive-include kontor/templates * diff --git a/python/kontor-cli/Makefile b/python/kontor-cli/Makefile new file mode 100644 index 0000000..b016c3c --- /dev/null +++ b/python/kontor-cli/Makefile @@ -0,0 +1,31 @@ +.PHONY: clean virtualenv test docker dist dist-upload + +clean: + find . -name '*.py[co]' -delete + +virtualenv: + virtualenv --prompt '|> kontor <| ' env + env/bin/pip install -r requirements-dev.txt + env/bin/python setup.py develop + @echo + @echo "VirtualENV Setup Complete. Now run: source env/bin/activate" + @echo + +test: + python -m pytest \ + -v \ + --cov=kontor \ + --cov-report=term \ + --cov-report=html:coverage-report \ + tests/ + +docker: clean + docker build -t kontor:latest . + +dist: clean + rm -rf dist/* + python setup.py sdist + python setup.py bdist_wheel + +dist-upload: + twine upload dist/* diff --git a/python/kontor-cli/README.md b/python/kontor-cli/README.md new file mode 100644 index 0000000..a95f4ea --- /dev/null +++ b/python/kontor-cli/README.md @@ -0,0 +1,69 @@ +# Kontor CLI + +## Installation + +``` +$ pip install -r requirements.txt + +$ python setup.py install +``` + +## Development + +This project includes a number of helpers in the `Makefile` to streamline common development tasks. + +### Environment Setup + +The following demonstrates setting up and working with a development environment: + +``` +### create a virtualenv for development + +$ make virtualenv + +$ source env/bin/activate + + +### run kontor cli application + +$ kontor --help + + +### run pytest / coverage + +$ make test +``` + + +### Releasing to PyPi + +Before releasing to PyPi, you must configure your login credentials: + +**~/.pypirc**: + +``` +[pypi] +username = YOUR_USERNAME +password = YOUR_PASSWORD +``` + +Then use the included helper function via the `Makefile`: + +``` +$ make dist + +$ make dist-upload +``` + +## Deployments + +### Docker + +Included is a basic `Dockerfile` for building and distributing `Kontor CLI`, +and can be built with the included `make` helper: + +``` +$ make docker + +$ docker run -it kontor --help +``` diff --git a/python/kontor-cli/config/kontor.yml.example b/python/kontor-cli/config/kontor.yml.example new file mode 100644 index 0000000..6806151 --- /dev/null +++ b/python/kontor-cli/config/kontor.yml.example @@ -0,0 +1,46 @@ +### Kontor CLI Configuration Settings +--- + +kontor: + +### Toggle application level debug (does not toggle framework debugging) +# debug: false + +### Where external (third-party) plugins are loaded from +# plugin_dir: /var/lib/kontor/plugins/ + +### Where all plugin configurations are loaded from +# plugin_config_dir: /etc/kontor/plugins.d/ + +### Where external templates are loaded from +# template_dir: /var/lib/kontor/templates/ + +### The log handler label +# log_handler: colorlog + +### The output handler label +# output_handler: jinja2 + +### sample foo option +# foo: bar + + +log.colorlog: + +### Where the log file lives (no log file by default) +# file: null + +### The level for which to log. One of: info, warning, error, fatal, debug +# level: info + +### Whether or not to log to console +# to_console: true + +### Whether or not to rotate the log file when it reaches `max_bytes` +# rotate: false + +### Max size in bytes that a log file can grow until it is rotated. +# max_bytes: 512000 + +### The maximum number of log files to maintain when rotating +# max_files: 4 diff --git a/python/kontor/templates/__init__.py b/python/kontor-cli/docs/.gitkeep similarity index 100% rename from python/kontor/templates/__init__.py rename to python/kontor-cli/docs/.gitkeep diff --git a/qt/gui/__init__.py b/python/kontor-cli/kontor/__init__.py similarity index 100% rename from qt/gui/__init__.py rename to python/kontor-cli/kontor/__init__.py diff --git a/python/kontor-cli/kontor/controllers/__init__.py b/python/kontor-cli/kontor/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/kontor-cli/kontor/controllers/base.py b/python/kontor-cli/kontor/controllers/base.py new file mode 100644 index 0000000..0f7bdda --- /dev/null +++ b/python/kontor-cli/kontor/controllers/base.py @@ -0,0 +1,60 @@ + +from cement import Controller, ex +from cement.utils.version import get_version_banner +from ..core.version import get_version + +VERSION_BANNER = """ +Kontor CLI %s +%s +""" % (get_version(), get_version_banner()) + + +class CliBase(Controller): + class Meta: + label = 'clibase' + + # text displayed at the top of --help output + description = 'Kontor CLI' + + # text displayed at the bottom of --help output + epilog = 'Usage: kontor command1 --foo bar' + + # controller level arguments. ex: 'kontor --version' + arguments = [ + ### add a version banner + ( [ '-v', '--version' ], + { 'action' : 'version', + 'version' : VERSION_BANNER } ), + ] + + + def _default(self): + """Default action if no sub-command is passed.""" + + self.app.args.print_help() + + + @ex( + help='example sub command1', + + # sub-command level arguments. ex: 'kontor command1 --foo bar' + arguments=[ + ### add a sample foo option under subcommand namespace + ( [ '-f', '--foo' ], + { 'help' : 'notorious foo option', + 'action' : 'store', + 'dest' : 'foo' } ), + ], + ) + def command1(self): + """Example sub-command.""" + + data = { + 'foo' : 'bar', + } + + ### do something with arguments + if self.app.pargs.foo is not None: + data['foo'] = self.app.pargs.foo + + self.app.render(data, 'command1.jinja2') diff --git a/python/kontor-cli/kontor/controllers/database.py b/python/kontor-cli/kontor/controllers/database.py new file mode 100644 index 0000000..73732f4 --- /dev/null +++ b/python/kontor-cli/kontor/controllers/database.py @@ -0,0 +1,51 @@ +from cement import Controller, ex +from kontor_schema import KontorDB + + +class Database(Controller): + + class Meta: + label = 'database' + stacked_type = 'nested' + stacked_on = 'clibase' + + @ex( + help='export database to given file', + arguments=[ + (['-f', '--file'], + {'help': 'file to store database content', + 'action': 'store', + 'dest': 'db_file'}) + ], + ) + def export(self): + data = { + 'db_file': 'data.json', + 'export_type': 'JSON', + } + if self.app.pargs.db_file is not None: + data['db_file'] = self.app.pargs.db_file + kontor_db = KontorDB(self.app.engine, self.app.config, self.app.log) + kontor_db.export_db(data['export_type'], data['db_file']) + self.app.render(data, 'command1.jinja2') + + @ex( + label='import', + help='import data from file into database', + arguments=[ + (['-f', '--file'], + {'help': 'file to read data', + 'action': 'store', + 'dest': 'db_file'}) + ], + ) + def import_cmd(self): + data = { + 'db_file': 'data.json', + 'data_type': 'JSON', + } + if self.app.pargs.db_file is not None: + data['db_file'] = self.app.pargs.db_file + kontor_db = KontorDB(self.app.engine, self.app.config, self.app.log) + self.app.render(data, 'import.jinja2') + kontor_db.import_db(data['db_file'], self.app.pargs.dry_run) diff --git a/python/kontor-cli/kontor/controllers/media.py b/python/kontor-cli/kontor/controllers/media.py new file mode 100644 index 0000000..7979b25 --- /dev/null +++ b/python/kontor-cli/kontor/controllers/media.py @@ -0,0 +1,81 @@ +from cement import Controller, ex +from kontor_schema import KontorDB +from kontor_video import VideoLink + +class Media(Controller): + + class Meta: + label = 'media' + stacked_type = 'nested' + stacked_on = 'clibase' + + @ex( + label='update', + help='update title for mediafiles', + ) + def update_title(self): + kontor_db = KontorDB(self.app.engine, self.app.log) + kontor_db.update_title(self.app.pargs.dry_run) + + @ex( + label='download', + help='download all marked videos', + arguments=[ + (['-d', '--dir'], + {'help': 'directory to store videos', + 'action': 'store', + 'dest': 'media_dir'}) + ], + ) + def download(self): + data = { + 'media_dir': '/data/media', + } + if self.app.pargs.media_dir is not None: + data['media_dir'] = self.app.pargs.media_dir + kontor_db = KontorDB(self.app.engine, self.app.log) + downloads = kontor_db.get_download_list() + for download in downloads: + link = VideoLink(download, download_dir=data['media_dir']) + link.download() + + @ex( + help='add url to database', + arguments=[ + (['-u', '--url'], + {'help': 'link to downloadable video', + 'action': 'store', + 'dest': 'link'}) + ], + ) + def add(self): + data = { + 'link_url': None + } + if self.app.pargs.link is not None: + data['link_url'] = self.app.pargs.link + if self.app.pargs.dry_run: + print(f"add url {data['link_url']} to database") + kontor_db = KontorDB(self.app.engine, self.app.log) + kontor_db.add_link(self.app.pargs.link, self.app.pargs.dry_run) + + else: + print("no url was given.") + + @ex( + help='check files if existing', + arguments=[ + (['-d', '--dir'], + {'help': 'directory to store videos', + 'action': 'store', + 'dest': 'media_dir'}) + ], + ) + def check(self): + data = { + 'media_dir': '/data/media', + } + if self.app.pargs.media_dir is not None: + data['media_dir'] = self.app.pargs.media_dir + kontor_db = KontorDB(self.app.engine, self.app.log) + kontor_db.check_files() diff --git a/python/kontor-cli/kontor/core/__init__.py b/python/kontor-cli/kontor/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/kontor/core/exc.py b/python/kontor-cli/kontor/core/exc.py similarity index 100% rename from python/kontor/core/exc.py rename to python/kontor-cli/kontor/core/exc.py diff --git a/python/kontor/core/version.py b/python/kontor-cli/kontor/core/version.py similarity index 100% rename from python/kontor/core/version.py rename to python/kontor-cli/kontor/core/version.py diff --git a/python/kontor-cli/kontor/ext/__init__.py b/python/kontor-cli/kontor/ext/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/kontor-cli/kontor/main.py b/python/kontor-cli/kontor/main.py new file mode 100644 index 0000000..8884b87 --- /dev/null +++ b/python/kontor-cli/kontor/main.py @@ -0,0 +1,118 @@ + +from cement import App, TestApp, init_defaults +from cement.core.exc import CaughtSignal +from kontor_schema.base import Base +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from .core.exc import KontorError +from .controllers.base import CliBase +from .controllers.database import Database +from .controllers.media import Media + +# configuration defaults +CONFIG = init_defaults('kontor', 'mariadb', 'media') +CONFIG['kontor']['foo'] = 'bar' +CONFIG['mariadb']['user'] = 'kontor' +CONFIG['mariadb']['password'] = 'kontor' +CONFIG['mariadb']['host'] = '127.0.0.1' +CONFIG['mariadb']['port'] = '3306' +CONFIG['mariadb']['database'] = 'kontor' +CONFIG['media']['yt-dlp'] = '/home/tpeetz/bin/yt-dlp' +CONFIG['media']['dir'] = '/data/media' + +def extend_sqlalchemy(app): + app.log.info('extending kontor application with sqlalchemy') + connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format( + app.config.get('mariadb', 'user'), + app.config.get('mariadb', 'password'), + app.config.get('mariadb', 'host'), + app.config.get('mariadb', 'port'), + app.config.get('mariadb', 'database') + )) + # engine = create_engine(connect_string, echo=True) + engine = create_engine(connect_string) + Base.metadata.create_all(bind=engine, checkfirst=True) + __session__ = sessionmaker(bind=engine) + app.extend('engine', engine) + + +class Kontor(App): + """Kontor CLI primary application.""" + + class Meta: + label = 'kontor' + + # configuration defaults + config_defaults = CONFIG + + # call sys.exit() on close + exit_on_close = True + + # load additional framework extensions + extensions = [ + 'yaml', + 'colorlog', + 'jinja2', + ] + + # configuration handler + config_handler = 'yaml' + + # configuration file suffix + config_file_suffix = '.yml' + + # set the log handler + log_handler = 'colorlog' + + # set the output handler + output_handler = 'jinja2' + + hooks = [ + ('post_setup', extend_sqlalchemy), + ] + + # register handlers + handlers = [ + CliBase, + Database, + Media, + ] + + +class KontorTest(TestApp,Kontor): + """A sub-class of Kontor that is better suited for testing.""" + + class Meta: + label = 'kontor' + + +def main(): + with Kontor() as app: + try: + app.run() + + except AssertionError as e: + print('AssertionError > %s' % e.args[0]) + app.exit_code = 1 + + if app.debug is True: + import traceback + traceback.print_exc() + + except KontorError as e: + print('KontorError > %s' % e.args[0]) + app.exit_code = 1 + + if app.debug is True: + import traceback + traceback.print_exc() + + except CaughtSignal as e: + # Default Cement signals are SIGINT and SIGTERM, exit 0 (non-error) + print('\n%s' % e) + app.exit_code = 0 + + +if __name__ == '__main__': + main() diff --git a/python/kontor-cli/kontor/plugins/__init__.py b/python/kontor-cli/kontor/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/kontor-cli/kontor/templates/__init__.py b/python/kontor-cli/kontor/templates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/kontor-cli/kontor/templates/command1.jinja2 b/python/kontor-cli/kontor/templates/command1.jinja2 new file mode 100644 index 0000000..2435e4d --- /dev/null +++ b/python/kontor-cli/kontor/templates/command1.jinja2 @@ -0,0 +1,4 @@ + +Example Template (templates/command1.jinja2) + +Foo => {{ foo }} diff --git a/python/kontor-cli/requirements-dev.txt b/python/kontor-cli/requirements-dev.txt new file mode 100644 index 0000000..f20606e --- /dev/null +++ b/python/kontor-cli/requirements-dev.txt @@ -0,0 +1,8 @@ +-r requirements.txt + +pytest +pytest-cov +coverage +twine>=1.11.0 +setuptools>=38.6.0 +wheel>=0.31.0 diff --git a/python/requirements.txt b/python/kontor-cli/requirements.txt similarity index 77% rename from python/requirements.txt rename to python/kontor-cli/requirements.txt index 76bba2b..5b4c054 100644 --- a/python/requirements.txt +++ b/python/kontor-cli/requirements.txt @@ -4,6 +4,4 @@ cement[yaml] cement[colorlog] mariadb sqlalchemy -PySide6 -beautifulsoup4 diff --git a/python/kontor-cli/setup.cfg b/python/kontor-cli/setup.cfg new file mode 100644 index 0000000..e69de29 diff --git a/python/kontor-cli/setup.py b/python/kontor-cli/setup.py new file mode 100644 index 0000000..18c2eba --- /dev/null +++ b/python/kontor-cli/setup.py @@ -0,0 +1,28 @@ + +from setuptools import setup, find_packages +from kontor.core.version import get_version + +VERSION = get_version() + +f = open('README.md', 'r') +LONG_DESCRIPTION = f.read() +f.close() + +setup( + name='kontor', + version=VERSION, + description='Kontor CLI', + long_description=LONG_DESCRIPTION, + long_description_content_type='text/markdown', + author='Thomas Peetz', + author_email='thomas.peetz@thpeetz.de', + url='https://gitlab.com/tpeetz/kontor/', + license='unlicensed', + packages=find_packages(exclude=['ez_setup', 'tests*']), + package_data={'kontor': ['templates/*']}, + include_package_data=True, + entry_points=""" + [console_scripts] + kontor = kontor.main:main + """, +) diff --git a/python/kontor-cli/tests/conftest.py b/python/kontor-cli/tests/conftest.py new file mode 100644 index 0000000..5124e2e --- /dev/null +++ b/python/kontor-cli/tests/conftest.py @@ -0,0 +1,16 @@ +""" +PyTest Fixtures. +""" + +import pytest +from cement import fs + +@pytest.fixture(scope="function") +def tmp(request): + """ + Create a `tmp` object that geneates a unique temporary directory, and file + for each test function that requires it. + """ + t = fs.Tmp() + yield t + t.remove() diff --git a/python/kontor-cli/tests/test_kontor.py b/python/kontor-cli/tests/test_kontor.py new file mode 100644 index 0000000..3c1bd67 --- /dev/null +++ b/python/kontor-cli/tests/test_kontor.py @@ -0,0 +1,36 @@ + +from pytest import raises +from kontor.main import KontorTest + +def test_kontor(): + # test kontor without any subcommands or arguments + with KontorTest() as app: + app.run() + assert app.exit_code == 0 + + +def test_kontor_debug(): + # test that debug mode is functional + argv = ['--debug'] + with KontorTest(argv=argv) as app: + app.run() + assert app.debug is True + + +def test_command1(): + # test command1 without arguments + argv = ['command1'] + with KontorTest(argv=argv) as app: + app.run() + data,output = app.last_rendered + assert data['foo'] == 'bar' + assert output.find('Foo => bar') + + + # test command1 with arguments + argv = ['command1', '--foo', 'not-bar'] + with KontorTest(argv=argv) as app: + app.run() + data,output = app.last_rendered + assert data['foo'] == 'not-bar' + assert output.find('Foo => not-bar') diff --git a/python/kontor-gui/.gitignore b/python/kontor-gui/.gitignore new file mode 100644 index 0000000..8d5ef37 --- /dev/null +++ b/python/kontor-gui/.gitignore @@ -0,0 +1,7 @@ +deployment/ +kontor.bin +bin/ +include/ +lib/ +lib64/ +lib64 diff --git a/python/kontor-gui/gui/__init__.py b/python/kontor-gui/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/kontor/gui/data_view.py b/python/kontor-gui/gui/data_view.py similarity index 100% rename from python/kontor/gui/data_view.py rename to python/kontor-gui/gui/data_view.py diff --git a/python/kontor/gui/data_view_model.py b/python/kontor-gui/gui/data_view_model.py similarity index 100% rename from python/kontor/gui/data_view_model.py rename to python/kontor-gui/gui/data_view_model.py diff --git a/python/kontor/gui/dialogs.py b/python/kontor-gui/gui/dialogs.py similarity index 100% rename from python/kontor/gui/dialogs.py rename to python/kontor-gui/gui/dialogs.py diff --git a/python/kontor/gui/main_window.py b/python/kontor-gui/gui/main_window.py similarity index 88% rename from python/kontor/gui/main_window.py rename to python/kontor-gui/gui/main_window.py index 0996d02..4f4ee91 100644 --- a/python/kontor/gui/main_window.py +++ b/python/kontor-gui/gui/main_window.py @@ -1,15 +1,10 @@ -from typing import Any - from PySide6.QtGui import QAction, QIcon from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView, QProgressBar from PySide6.QtWidgets import QLabel, QMainWindow -from cement.core.config import ConfigHandler from sqlalchemy import Engine +from kontor_schema import KontorDB from .progress import ProgressUpdate -from ..database import KontorDB -from ..database.media import MediaFile -from ..database.comic import Comic from .dialogs import ExportKontorDialog, ImportKontorDialog from .model_config import KontorModelConfig from .table_model import KontorTableModel @@ -17,14 +12,14 @@ from .table_model import KontorTableModel class MainWindow(QMainWindow): - def __init__(self, engine: Engine, config: ConfigHandler, log): + def __init__(self, engine: Engine, log): super().__init__() - self.tick = QIcon('kontor/res/tick.png') - self.cross = QIcon('kontor/res/cross.png') - self.import_icon = QIcon("kontor/res/application-import.png") - self.export_icon = QIcon("kontor/res/application-export.png") - self.circle_icon = QIcon("kontor/res/arrow-circle-double.png") + self.tick = QIcon('res/tick.png') + self.cross = QIcon('res/cross.png') + self.import_icon = QIcon("res/application-import.png") + self.export_icon = QIcon("res/application-export.png") + self.circle_icon = QIcon("res/arrow-circle-double.png") self.setWindowTitle("Kontor") self.setMinimumSize(800, 500) @@ -37,14 +32,14 @@ class MainWindow(QMainWindow): self.data = [] self.filter = {} - self.kontor_db = KontorDB(engine, config, log) + self.kontor_db = KontorDB(engine, log) self.log = log self.central_widget = QWidget() parent_layout = QVBoxLayout() self.central_widget.setLayout(parent_layout) self.tabs = QTabWidget() - self.tabs.addTab(self.generate_data_tab("comic", Comic), "Comics") - self.tabs.addTab(self.generate_data_tab("media_file", MediaFile), "MediaFile") + self.tabs.addTab(self.generate_data_tab("comic"), "Comics") + self.tabs.addTab(self.generate_data_tab("media_file"), "MediaFile") self.tabs.currentChanged.connect(self._tab_changed) #label.setAlignment(Qt.AlignmentFlag.AlignCenter) parent_layout.addWidget(self.tabs) @@ -158,9 +153,10 @@ class MainWindow(QMainWindow): def _tab_changed(self, tab_index): self.data[tab_index].refresh() - def generate_data_tab(self, table_name, table): + def generate_data_tab(self, table_name): data_tab = QWidget() - table_config = KontorModelConfig(self.kontor_db, self, table_name, table) + + table_config = KontorModelConfig(self.kontor_db, self, table_name) model = KontorTableModel(table_config) layout = QVBoxLayout() self.data.append(model) diff --git a/python/kontor/gui/model_config.py b/python/kontor-gui/gui/model_config.py similarity index 90% rename from python/kontor/gui/model_config.py rename to python/kontor-gui/gui/model_config.py index ce61107..e791752 100644 --- a/python/kontor/gui/model_config.py +++ b/python/kontor-gui/gui/model_config.py @@ -1,17 +1,14 @@ -import mariadb from PySide6.QtWidgets import QHBoxLayout, QCheckBox - -from ..database import KontorDB +from kontor_schema import KontorDB class KontorModelConfig: - def __init__(self, kontor_db: KontorDB, main_window, table_name: str, table): + def __init__(self, kontor_db: KontorDB, main_window, table_name: str): self.header = {} self.filter = {} self.main_window = main_window self._table_name = table_name - self._table = table self.kontor_db = kontor_db self.get_table_config() @@ -32,7 +29,7 @@ class KontorModelConfig: def get_data(self) -> list: # data = self.kontor_db.get_data(self._table_name, self.header, self.get_filter()) # data.clear() - data = self.kontor_db.data(self._table, self.header, self.filters()) + data = self.kontor_db.data(self._table_name, self.header, self.filters()) # print(f"KontorModelConfig.get_data: {len(data)}") # comics = self.kontor_db.session.query(Comic).all() # print(f'{len(comics)} Comics loaded') diff --git a/python/kontor/gui/progress.py b/python/kontor-gui/gui/progress.py similarity index 100% rename from python/kontor/gui/progress.py rename to python/kontor-gui/gui/progress.py diff --git a/python/kontor/gui/table_model.py b/python/kontor-gui/gui/table_model.py similarity index 100% rename from python/kontor/gui/table_model.py rename to python/kontor-gui/gui/table_model.py diff --git a/python/kontor-gui/kontor.py b/python/kontor-gui/kontor.py new file mode 100644 index 0000000..50e16cd --- /dev/null +++ b/python/kontor-gui/kontor.py @@ -0,0 +1,43 @@ +""" +PyQT6 GUI for Kontor +""" +import logging +import sys +import logging.config +from pathlib import Path +from platformdirs import PlatformDirs +from PySide6.QtWidgets import QApplication +import yaml +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from kontor_schema.base import Base + +from gui.main_window import MainWindow + + +if __name__ == '__main__': + app = QApplication(sys.argv) + dirs = PlatformDirs("kontor") + database_config = Path(dirs.user_config_dir, 'database-config.yaml') + with open(database_config, 'rt') as f: + db_config = yaml.safe_load(f.read()) + connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format( + db_config['mariadb']['user'], + db_config['mariadb']['password'], + db_config['mariadb']['host'], + db_config['mariadb']['port'], + db_config['mariadb']['database'] + )) + logging_config = Path(dirs.user_config_dir, 'logging-config.yaml') + with open(logging_config, 'rt') as f: + config = yaml.safe_load(f.read()) + logging.config.dictConfig(config) + logger = logging.getLogger('development') + # engine = create_engine(connect_string, echo=True) + engine = create_engine(connect_string) + Base.metadata.create_all(bind=engine, checkfirst=True) + __session__ = sessionmaker(bind=engine) + + window = MainWindow(engine, logger) + window.show() + app.exec() diff --git a/python/kontor-gui/pyvenv.cfg b/python/kontor-gui/pyvenv.cfg new file mode 100644 index 0000000..e6324f5 --- /dev/null +++ b/python/kontor-gui/pyvenv.cfg @@ -0,0 +1,5 @@ +home = /usr/bin +include-system-site-packages = false +version = 3.11.2 +executable = /usr/bin/python3.11 +command = /usr/bin/python -m venv /home/tpeetz/projects/kontor/python/kontor-gui diff --git a/python/kontor-gui/requirements.txt b/python/kontor-gui/requirements.txt new file mode 100644 index 0000000..90b0945 --- /dev/null +++ b/python/kontor-gui/requirements.txt @@ -0,0 +1,6 @@ +-e /home/tpeetz/projects/kontor/python/kontor-schema +-e /home/tpeetz/projects/kontor/python/kontor-video + +platformdirs +pyyaml +PySide6 diff --git a/python/kontor/res/application-export.png b/python/kontor-gui/res/application-export.png similarity index 100% rename from python/kontor/res/application-export.png rename to python/kontor-gui/res/application-export.png diff --git a/python/kontor/res/application-import.png b/python/kontor-gui/res/application-import.png similarity index 100% rename from python/kontor/res/application-import.png rename to python/kontor-gui/res/application-import.png diff --git a/python/kontor/res/arrow-circle-double.png b/python/kontor-gui/res/arrow-circle-double.png similarity index 100% rename from python/kontor/res/arrow-circle-double.png rename to python/kontor-gui/res/arrow-circle-double.png diff --git a/python/kontor/res/cross.png b/python/kontor-gui/res/cross.png similarity index 100% rename from python/kontor/res/cross.png rename to python/kontor-gui/res/cross.png diff --git a/python/kontor/res/tick.png b/python/kontor-gui/res/tick.png similarity index 100% rename from python/kontor/res/tick.png rename to python/kontor-gui/res/tick.png diff --git a/python/kontor-schema/README.md b/python/kontor-schema/README.md new file mode 100644 index 0000000..eafef62 --- /dev/null +++ b/python/kontor-schema/README.md @@ -0,0 +1,4 @@ +# Schema for Kontor DB + +This library contains the schema for the Kontor DB. + diff --git a/python/kontor-schema/include/site/python3.11/greenlet/greenlet.h b/python/kontor-schema/include/site/python3.11/greenlet/greenlet.h new file mode 100644 index 0000000..d02a16e --- /dev/null +++ b/python/kontor-schema/include/site/python3.11/greenlet/greenlet.h @@ -0,0 +1,164 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ + +/* Greenlet object interface */ + +#ifndef Py_GREENLETOBJECT_H +#define Py_GREENLETOBJECT_H + + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* This is deprecated and undocumented. It does not change. */ +#define GREENLET_VERSION "1.0.0" + +#ifndef GREENLET_MODULE +#define implementation_ptr_t void* +#endif + +typedef struct _greenlet { + PyObject_HEAD + PyObject* weakreflist; + PyObject* dict; + implementation_ptr_t pimpl; +} PyGreenlet; + +#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type)) + + +/* C API functions */ + +/* Total number of symbols that are exported */ +#define PyGreenlet_API_pointers 12 + +#define PyGreenlet_Type_NUM 0 +#define PyExc_GreenletError_NUM 1 +#define PyExc_GreenletExit_NUM 2 + +#define PyGreenlet_New_NUM 3 +#define PyGreenlet_GetCurrent_NUM 4 +#define PyGreenlet_Throw_NUM 5 +#define PyGreenlet_Switch_NUM 6 +#define PyGreenlet_SetParent_NUM 7 + +#define PyGreenlet_MAIN_NUM 8 +#define PyGreenlet_STARTED_NUM 9 +#define PyGreenlet_ACTIVE_NUM 10 +#define PyGreenlet_GET_PARENT_NUM 11 + +#ifndef GREENLET_MODULE +/* This section is used by modules that uses the greenlet C API */ +static void** _PyGreenlet_API = NULL; + +# define PyGreenlet_Type \ + (*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM]) + +# define PyExc_GreenletError \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM]) + +# define PyExc_GreenletExit \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM]) + +/* + * PyGreenlet_New(PyObject *args) + * + * greenlet.greenlet(run, parent=None) + */ +# define PyGreenlet_New \ + (*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \ + _PyGreenlet_API[PyGreenlet_New_NUM]) + +/* + * PyGreenlet_GetCurrent(void) + * + * greenlet.getcurrent() + */ +# define PyGreenlet_GetCurrent \ + (*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM]) + +/* + * PyGreenlet_Throw( + * PyGreenlet *greenlet, + * PyObject *typ, + * PyObject *val, + * PyObject *tb) + * + * g.throw(...) + */ +# define PyGreenlet_Throw \ + (*(PyObject * (*)(PyGreenlet * self, \ + PyObject * typ, \ + PyObject * val, \ + PyObject * tb)) \ + _PyGreenlet_API[PyGreenlet_Throw_NUM]) + +/* + * PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args) + * + * g.switch(*args, **kwargs) + */ +# define PyGreenlet_Switch \ + (*(PyObject * \ + (*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \ + _PyGreenlet_API[PyGreenlet_Switch_NUM]) + +/* + * PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent) + * + * g.parent = new_parent + */ +# define PyGreenlet_SetParent \ + (*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \ + _PyGreenlet_API[PyGreenlet_SetParent_NUM]) + +/* + * PyGreenlet_GetParent(PyObject* greenlet) + * + * return greenlet.parent; + * + * This could return NULL even if there is no exception active. + * If it does not return NULL, you are responsible for decrementing the + * reference count. + */ +# define PyGreenlet_GetParent \ + (*(PyGreenlet* (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_GET_PARENT_NUM]) + +/* + * deprecated, undocumented alias. + */ +# define PyGreenlet_GET_PARENT PyGreenlet_GetParent + +# define PyGreenlet_MAIN \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_MAIN_NUM]) + +# define PyGreenlet_STARTED \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_STARTED_NUM]) + +# define PyGreenlet_ACTIVE \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_ACTIVE_NUM]) + + + + +/* Macro that imports greenlet and initializes C API */ +/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we + keep the older definition to be sure older code that might have a copy of + the header still works. */ +# define PyGreenlet_Import() \ + { \ + _PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \ + } + +#endif /* GREENLET_MODULE */ + +#ifdef __cplusplus +} +#endif +#endif /* !Py_GREENLETOBJECT_H */ diff --git a/python/kontor/database/__init__.py b/python/kontor-schema/kontor_schema/__init__.py similarity index 94% rename from python/kontor/database/__init__.py rename to python/kontor-schema/kontor_schema/__init__.py index 54c296b..3549955 100644 --- a/python/kontor/database/__init__.py +++ b/python/kontor-schema/kontor_schema/__init__.py @@ -4,29 +4,22 @@ import subprocess import uuid from datetime import datetime from pathlib import Path -from typing import Any -import requests -from bs4 import BeautifulSoup -from cement.core.config import ConfigHandler from sqlalchemy import Engine from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import sessionmaker -from .base import Base from .bookshelf import Article, Book, Author, BookshelfPublisher, ArticleAuthor, BookAuthor from .comic import Comic, Artist, Publisher, Issue, StoryArc, TradePaperback, Volume, ComicWork, WorkType from .metadata import MetaDataTable, MetaDataColumn from .tysc import Card, CardSet, Sport, Team, FieldPosition, Rooster, Player, Vendor from .media import MediaFile, MediaArticle, MediaVideo -from ..gui.progress import ProgressUpdate class KontorDB: - def __init__(self, db_engine: Engine, config: ConfigHandler, log): + def __init__(self, db_engine: Engine, log): self.engine = db_engine - self.config = config self.log = log self.registry = {} self.init_registry() @@ -106,9 +99,10 @@ class KontorDB: self.log.debug(f"retrieved {len(_filter_map)} filters: {_filter_map}") return _filter_map - def data(self, table, columns: dict, filters) -> list: + def data(self, table_name: str, columns: dict, filters: dict) -> list: data = [] __session__ = sessionmaker(self.engine) + table = self.registry[table_name] with __session__() as session: entries = [] if len(filters) == 0: @@ -306,8 +300,23 @@ class KontorDB: link.review = 0 session.commit() - def download_file(self, dry_run=False, update: ProgressUpdate=None): - self.log.info(f"download marked files of media_file") + def get_download_list(self) -> list[str]: + self.log.debug("get links marked as should_download") + download_list = [] + __session__ = sessionmaker(self.engine) + with __session__() as session: + links = session.query(MediaFile).filter(MediaFile.should_download == 1).all() + for link in links: + url = link.url + if url is None: + self.log.info(f"url has not been set for {link.id}") + continue + download_list.append(url) + self.log.debug(f"found {len(download_list)} urls for downloads") + return download_list + + def download_file(self, dry_run=False): + self.log.info(f"download marked urls of media_file") __session__ = sessionmaker(self.engine) with __session__() as session: links = session.query(MediaFile).filter(MediaFile.should_download == 1).all() diff --git a/python/kontor/database/base.py b/python/kontor-schema/kontor_schema/base.py similarity index 100% rename from python/kontor/database/base.py rename to python/kontor-schema/kontor_schema/base.py diff --git a/python/kontor/database/bookshelf.py b/python/kontor-schema/kontor_schema/bookshelf.py similarity index 100% rename from python/kontor/database/bookshelf.py rename to python/kontor-schema/kontor_schema/bookshelf.py diff --git a/python/kontor/database/comic.py b/python/kontor-schema/kontor_schema/comic.py similarity index 100% rename from python/kontor/database/comic.py rename to python/kontor-schema/kontor_schema/comic.py diff --git a/python/kontor/database/media.py b/python/kontor-schema/kontor_schema/media.py similarity index 100% rename from python/kontor/database/media.py rename to python/kontor-schema/kontor_schema/media.py diff --git a/python/kontor/database/metadata.py b/python/kontor-schema/kontor_schema/metadata.py similarity index 100% rename from python/kontor/database/metadata.py rename to python/kontor-schema/kontor_schema/metadata.py diff --git a/python/kontor/database/tysc.py b/python/kontor-schema/kontor_schema/tysc.py similarity index 100% rename from python/kontor/database/tysc.py rename to python/kontor-schema/kontor_schema/tysc.py diff --git a/python/kontor-schema/pyvenv.cfg b/python/kontor-schema/pyvenv.cfg new file mode 100644 index 0000000..794f3c8 --- /dev/null +++ b/python/kontor-schema/pyvenv.cfg @@ -0,0 +1,5 @@ +home = /usr/bin +include-system-site-packages = false +version = 3.11.2 +executable = /usr/bin/python3.11 +command = /usr/bin/python -m venv /home/tpeetz/projects/kontor/python/kontor-schema diff --git a/python/kontor-schema/requirements.txt b/python/kontor-schema/requirements.txt new file mode 100644 index 0000000..d08ec81 --- /dev/null +++ b/python/kontor-schema/requirements.txt @@ -0,0 +1,2 @@ +mariadb +sqlalchemy diff --git a/python/kontor-schema/setup.py b/python/kontor-schema/setup.py new file mode 100644 index 0000000..7f76710 --- /dev/null +++ b/python/kontor-schema/setup.py @@ -0,0 +1,23 @@ +from setuptools import setup, find_packages +import pathlib + +here = pathlib.Path(__file__).parent.resolve() + +long_description = ( here / "README.md").read_text(encoding="utf-8") + +setup( + name='kontor_schema', + version='0.1.0', + description='Schema for Konotor DB', + long_description=long_description, + long_description_content_type="text/markdown", + author='Thomas Peetz', + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.11", + ], + install_requires=["sqlalchemy", "mariadb"], + packages=find_packages(), +) diff --git a/python/kontor-video/README.md b/python/kontor-video/README.md new file mode 100644 index 0000000..c52965f --- /dev/null +++ b/python/kontor-video/README.md @@ -0,0 +1,3 @@ +# Kontor Video + +This project provides helper methods to handle video links, like Youtube or ZDF Mediathek. diff --git a/python/kontor-video/kontor_video/__init__.py b/python/kontor-video/kontor_video/__init__.py new file mode 100644 index 0000000..6fd7843 --- /dev/null +++ b/python/kontor-video/kontor_video/__init__.py @@ -0,0 +1,21 @@ +import requests +from bs4 import BeautifulSoup + + +class VideoLink: + + def __init__(self, url: str, log): + self.url = url + self.title = None + self.log = log + + def get_title(self): + try: + r = requests.get(self.url) + soup = BeautifulSoup(r.content, "html.parser") + title = soup.title.string + except: + self.log.info("Sorry, could not retrieve title") + + def download(self, download_dir=None): + self.log.info(f"download {self.url} to {download_dir}") diff --git a/python/kontor-video/pyvenv.cfg b/python/kontor-video/pyvenv.cfg new file mode 100644 index 0000000..e789070 --- /dev/null +++ b/python/kontor-video/pyvenv.cfg @@ -0,0 +1,5 @@ +home = /usr/bin +include-system-site-packages = false +version = 3.11.2 +executable = /usr/bin/python3.11 +command = /usr/bin/python -m venv /home/tpeetz/projects/kontor/python/kontor-video diff --git a/python/kontor-video/requirements.txt b/python/kontor-video/requirements.txt new file mode 100644 index 0000000..1f3e778 --- /dev/null +++ b/python/kontor-video/requirements.txt @@ -0,0 +1,2 @@ +beautifulsoup4 +requests diff --git a/python/kontor-video/setup.py b/python/kontor-video/setup.py new file mode 100644 index 0000000..1362fdf --- /dev/null +++ b/python/kontor-video/setup.py @@ -0,0 +1,23 @@ +from setuptools import setup, find_packages +import pathlib + +here = pathlib.Path(__file__).parent.resolve() + +long_description = ( here / "README.md").read_text(encoding="utf-8") + +setup( + name='kontor_video', + version='0.1.0', + description='Helper methods to download videos', + long_description=long_description, + long_description_content_type="text/markdown", + author='Thomas Peetz', + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.11", + ], + install_requires=["beautifulsoup4"], + packages=find_packages(), +) diff --git a/qt/.gitignore b/qt/.gitignore deleted file mode 100644 index 38b154f..0000000 --- a/qt/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -deployment/ -kontor.bin diff --git a/qt/database/__init__.py b/qt/database/__init__.py deleted file mode 100644 index 47f5706..0000000 --- a/qt/database/__init__.py +++ /dev/null @@ -1,212 +0,0 @@ -import json -from datetime import datetime -from pathlib import Path - -import mariadb -from sqlalchemy import create_engine, select, text, MetaData, join -from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker - -from .base import Base -from .comic import Comic, Artist, Publisher, ComicWork, WorkType, StoryArc, Volume, Issue, TradePaperback -from .tysc import Sport, Team, Card, CardSet, Vendor, Rooster, Player, FieldPosition -from .media import MediaFile -from .metadata import MetaDataTable, MetaDataColumn - - -class KontorDB: - - def __init__(self, db_config): - self.db_conn = mariadb.connect( - host=db_config['mariadb']['host'], - port=db_config['mariadb']['port'], - user=db_config['mariadb']['user'], - password=db_config['mariadb']['password'], - database=db_config['mariadb']['database'] - ) - connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}' - .format( - db_config['mariadb']['user'], - db_config['mariadb']['password'], - db_config['mariadb']['host'], - db_config['mariadb']['port'], - db_config['mariadb']['database'])) - # engine = create_engine(connect_string, echo=True) - engine = create_engine(connect_string) - Base.metadata.create_all(bind=engine) - __session__ = sessionmaker(bind=engine) - self.session = __session__() - self.registry = {} - self.init_registry() - - def init_registry(self): - self.registry['card'] = Card - self.registry['card_set'] = CardSet - self.registry['sport'] = Sport - self.registry['team'] = Team - self.registry['field_position'] = FieldPosition - self.registry['rooster'] = Rooster - self.registry['player'] = Player - self.registry['vendor'] = Vendor - self.registry['artist'] = Artist - self.registry['publisher'] = Publisher - self.registry['comic'] = Comic - self.registry['issue'] = Issue - self.registry['story_arc'] = StoryArc - self.registry['trade_paperback'] = TradePaperback - self.registry['volume'] = Volume - self.registry['comic_work'] = ComicWork - self.registry['worktype'] = WorkType - self.registry['media_file'] = MediaFile - - - def get_table_names(self) -> list: - tables = self.session.query(MetaDataTable).all() - result = [table.table_name for table in tables] - return result - - def get_column_meta_data(self, table_name: str, view_only=True) -> dict: - meta_data = {} - order = 0 - if view_only: - for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). - filter(MetaDataTable.id == MetaDataColumn.table_id). - filter(MetaDataTable.table_name == table_name). - filter(MetaDataColumn.is_shown == 1).all()): - meta_data[order] = {'column': column.column_name, 'label': column.column_label, - 'order': column.column_order, 'ref_column': column.ref_column} - order += 1 - else: - for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). - filter(MetaDataTable.id == MetaDataColumn.table_id). - filter(MetaDataTable.table_name == table_name).all()): - meta_data[order] = { - 'column': column.column_name, - 'order': column.column_order, - 'ref_column': column.ref_column - } - order += 1 - return meta_data - - def get_filters(self, table_name): - _filter_map = {} - for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). - filter(MetaDataTable.id == MetaDataColumn.table_id). - filter(MetaDataTable.table_name == table_name). - filter(MetaDataColumn.show_filter == 1).all()): - _filter_map[column.column_name] = {'label': column.filter_label, 'widget': None} - print(f"retrieved {len(_filter_map)} filters: {_filter_map}") - return _filter_map - - def data(self, table, columns: dict, filters) -> list: - data = [] - entries = [] - if len(filters) == 0: - entries = self.session.query(table).all() - else: - entries = self.session.query(table).filter_by(**filters) - for entry in entries: - row = [] - for order in columns.keys(): - column_name = columns[order]['column'] - if str(column_name).endswith("_id"): - ref_table = column_name[:-3] - # print(f"{ref_table=}") - ref = getattr(entry, ref_table) - value = getattr(ref, "name") - # print(f"{value=}") - row.append(value) - else: - row.append(getattr(entry, column_name)) - # print(repr(row)) - data.append(row) - return data - - def get_data(self, table_name: str, columns: dict, where_clause: str) -> list: - data = [] - cursor = self.db_conn.cursor() - cursor.execute(self.get_statement(table_name, columns, where_clause)) - rows = cursor.fetchall() - print(len(rows)) - for row in rows: - # print(f"KontorDB.get_data: {row}") - data.append(list(row)) - cursor.close() - # print(f"KontorDB.getData: return {len(data)}") - if table_name == 'comic' and len(where_clause) == 0: - data.clear() - comics = self.session.query(Comic).all() - for item in comics: - # print(item) - row = [] - for order in columns.keys(): - column_name = columns[order]['column'] - if str(column_name).endswith("_id"): - ref_table = column_name[:-3] - # print(f"{ref_table=}") - ref = getattr(item, ref_table) - value = getattr(ref, "name") - # print(f"{value=}") - row.append(value) - else: - row.append(getattr(item, column_name)) - # print(repr(row)) - data.append(row) - return data - - def get_statement(self, table: str, header: dict, where_clause): - columns = "" - for index, column in header.items(): - if index > 0: - columns += ", " - columns += column['column'] - if len(columns) == 0: - columns = "*" - statement = f"SELECT {columns} FROM {table} {where_clause}" - print(f"{statement=}") - return statement - - def export_db(self, export_type: str, export_file_name: str, export_table_list: list): - print(f"export DB to {export_file_name} as {export_type}") - db = {} - for table in export_table_list: - columns = self.get_column_meta_data(table, view_only=False) - if table in self.registry: - model = self.registry[table] - else: - print(f"table {table} is not registered") - continue - rows = self.session.query(model).all() - entries = [] - print(f"found {len(rows)} entries") - print(f"found {len(columns)} columns") - for row in rows: - # print(row) - entry = {} - for order in columns: - # print(columns[order]) - column_name = columns[order]['column'] - # print(f"get value {column_name} from {row} of table {table}") - try: - value = getattr(row, column_name) - if isinstance(value, datetime): - entry[column_name] = str(value) - else: - entry[column_name] = value - except AttributeError as error: - print("could not get value") - entries.append(entry) - db[table] = entries - export_file = Path(export_file_name) - match export_type: - case "JSON": - json_dump = json.dumps(db, indent=4) - with open(export_file_name, "w") as dump_file: - dump_file.write(json_dump) - case "YAML": - export_file = Path(export_file_name) - case "SQLite": - export_file = Path(export_file_name) - case _: - print("unknown export type") - if export_file.exists(): - print(f"{export_file} exists") diff --git a/qt/database/base.py b/qt/database/base.py deleted file mode 100644 index 97d2990..0000000 --- a/qt/database/base.py +++ /dev/null @@ -1,18 +0,0 @@ -from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker, declarative_base - - -class Base(DeclarativeBase): - pass - -# class BaseModel: -# -# @classmethod -# def model_lookup_by_table_name(cls, table_name): -# registry_instance = getattr(cls, "registry") -# for mapper_ in registry_instance.mappers: -# model = mapper_.class_ -# model_class_name = model.__tablename__ -# if model_class_name == table_name: -# return model - -# Base = declarative_base(cls=BaseModel) \ No newline at end of file diff --git a/qt/database/comic.py b/qt/database/comic.py deleted file mode 100644 index 49d222f..0000000 --- a/qt/database/comic.py +++ /dev/null @@ -1,130 +0,0 @@ -from sqlalchemy import Column, DateTime, ForeignKey, Integer, String -from sqlalchemy.dialects.mysql import BIT -from sqlalchemy.orm import relationship - -from .base import Base - - -class Publisher(Base): - __tablename__ = "publisher" - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) - name = Column(String(length=255), unique=True) - comics = relationship("Comic") - - def __repr__(self): - return f'Publisher({self.id} {self.name})' - - def __str__(self): - return self.__repr__() - - -class Comic(Base): - __tablename__ = 'comic' - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) - title = Column(String(length=255), unique=True) - publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False) - publisher = relationship("Publisher", back_populates="comics") - current_order = Column(BIT(1)) - completed = Column(BIT(1)) - issues = relationship("Issue") - story_arcs = relationship("StoryArc") - trade_paperbacks = relationship("TradePaperback") - volumes = relationship("Volume") - comic_works = relationship("ComicWork") - - def __repr__(self): - return f'Comic({self.id} {self.version} {self.title} {self.publisher.name})' - - def __str__(self): - return f'{self.title}({self.id})' - - -class Volume(Base): - __tablename__ = "volume" - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) - name = Column(String(length=255), nullable=False) - comic_id = Column(String, ForeignKey("comic.id"), nullable=False) - comic = relationship("Comic", back_populates="volumes") - issues = relationship("Issue") - - -class TradePaperback(Base): - __tablename__ = "trade_paperback" - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) - name = Column(String(length=255), nullable=False) - issue_start = Column(Integer) - issue_end = Column(Integer) - comic_id = Column(String, ForeignKey("comic.id"), nullable=False) - comic = relationship("Comic", back_populates="trade_paperbacks") - - -class StoryArc(Base): - __tablename__ = "story_arc" - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) - name = Column(String(length=255), nullable=False) - comic_id = Column(String, ForeignKey("comic.id"), nullable=False) - comic = relationship("Comic", back_populates="story_arcs") - - -class Issue(Base): - __tablename__ = "issue" - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) - issue_number = Column(String(255)) - in_stock = Column(BIT(1)) - is_read = Column(BIT(1)) - comic_id = Column(String, ForeignKey("comic.id"), nullable=False) - comic = relationship("Comic", back_populates="issues") - volume_id = Column(String, ForeignKey("volume.id"), nullable=True) - volume = relationship("Volume", back_populates="issues") - - -class Artist(Base): - __tablename__ = "artist" - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) - name = Column(String(length=255), nullable=False) - comic_works = relationship("ComicWork") - - -class WorkType(Base): - __tablename__ = "worktype" - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) - name = Column(String(length=255), nullable=False, unique=True) - comic_works = relationship("ComicWork") - - -class ComicWork(Base): - __tablename__ = "comic_work" - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) - comic_id = Column(String, ForeignKey("comic.id"), nullable=False) - comic = relationship("Comic", back_populates="comic_works") - artist_id = Column(String, ForeignKey("artist.id"), nullable=False) - artist = relationship("Artist", back_populates="comic_works") - work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False) - work_type = relationship("WorkType", back_populates="comic_works") diff --git a/qt/database/media.py b/qt/database/media.py deleted file mode 100644 index b5d793b..0000000 --- a/qt/database/media.py +++ /dev/null @@ -1,25 +0,0 @@ -from sqlalchemy import Column, DateTime, Integer, String -from sqlalchemy.dialects.mysql import BIT - -from database.base import Base - - -class MediaFile(Base): - __tablename__ = 'media_file' - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) - cloud_link = Column(String(255)) - file_name = Column(String(255)) - path = Column(String(255)) - review = Column(BIT(1)) - title = Column(String(255)) - url = Column(String(255)) - should_download = Column(BIT(1)) - - def __repr__(self): - return f'MediaFile({self.id} {self.title} {self.title})' - - def __str__(self): - return f'{self.title}({self.id})' diff --git a/qt/database/metadata.py b/qt/database/metadata.py deleted file mode 100644 index 46a6274..0000000 --- a/qt/database/metadata.py +++ /dev/null @@ -1,49 +0,0 @@ -from sqlalchemy import Column, String, ForeignKey, DateTime, Integer, Boolean -from sqlalchemy.dialects.mysql import BIT -from sqlalchemy.orm import relationship - -from database import Base - - -class MetaDataTable(Base): - __tablename__ = 'meta_data_table' - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) - table_name = Column(String(255), unique=True) - table_columns = relationship("MetaDataColumn") - - def __repr__(self): - return f'MetaDataTable({self.id} {self.table_name})' - - def __str__(self): - return f'{self.table_name}({self.id})' - -class MetaDataColumn(Base): - __tablename__ = 'meta_data_column' - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) - column_modifier = Column(String(255), nullable=True) - column_name = Column(String(255)) - column_order = Column(Integer) - column_sync_name = Column(String(255)) - column_type = Column(String(255)) - table_id = Column(String, ForeignKey('meta_data_table.id')) - table = relationship("MetaDataTable", back_populates="table_columns") - column_label = Column(String(255)) - filter_label = Column(String(255)) - is_shown = Column(BIT(1)) - show_filter = Column(BIT(1)) - ref_column = Column(String, nullable=True) - - def __repr__(self): - if self.column_name is None: - return f'MetaDataColumn({self.id} {self.table.table_name}.__)' - else: - return f'MetaDataColumn({self.id} {self.table.table_name}.{self.column_name})' - - def __str__(self): - return f'{self.column_name}({self.id})' diff --git a/qt/database/tysc.py b/qt/database/tysc.py deleted file mode 100644 index 733d7ed..0000000 --- a/qt/database/tysc.py +++ /dev/null @@ -1,131 +0,0 @@ -from sqlalchemy import Column, DateTime, Integer, String, ForeignKey, UniqueConstraint -from sqlalchemy.dialects.mysql import BIT -from sqlalchemy.orm import relationship - -from .base import Base - - -class Sport(Base): - __tablename__ = "sport" - __table_args__ = ( - UniqueConstraint("name"), - ) - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) - name = Column(String(255), nullable=False, index=True, unique=True) - teams = relationship("Team") - positions = relationship("FieldPosition") - - -class Team(Base): - __tablename__ = "team" - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) - name = Column(String(255), nullable=False, index=True, unique=True) - short_name = Column(String(255), nullable=False, ) - sport_id = Column(String, ForeignKey("sport.id"), nullable=False) - sport = relationship("Sport", back_populates="teams") - roosters = relationship("Rooster") - - -class FieldPosition(Base): - __tablename__ = "field_position" - __table_args__ = ( - UniqueConstraint("name", "sport_id"), - UniqueConstraint("short_name", "sport_id"), - ) - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) - name = Column(String(255), nullable=False, index=True) - short_name = Column(String(255), nullable=False) - sport_id = Column(String, ForeignKey("sport.id"), nullable=False, index=True) - sport = relationship("Sport", back_populates="positions") - roosters = relationship("Rooster") - - -class Player(Base): - __tablename__ = "player" - __table_args__ = ( - UniqueConstraint("first_name", "last_name"), - ) - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) - first_name = Column(String(255), nullable=False, index=True) - last_name = Column(String(255), nullable=False, index=True) - roosters = relationship("Rooster") - - def get_full_name(self) -> str: - return f"{self.last_name}, {self.first_name}" - - -class Rooster(Base): - __tablename__ = "rooster" - __table_args__ = ( - UniqueConstraint("year", "team_id", "player_id", "position_id"), - ) - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) - year = Column(Integer) - team_id = Column(String, ForeignKey("team.id"), nullable=False, index=True) - team = relationship("Team", back_populates="roosters") - player_id = Column(String, ForeignKey("player.id"), nullable=False, index=True) - player = relationship("Player", back_populates="roosters") - position_id = Column(String, ForeignKey("field_position.id"), nullable=False, index=True) - position = relationship("FieldPosition", back_populates="roosters") - cards = relationship("Card") - -class Vendor(Base): - __tablename__ = "vendor" - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) - name = Column(String(255), nullable=False, unique=True, index=True) - card_sets = relationship("CardSet") - cards = relationship("Card") - - -class CardSet(Base): - __tablename__ = "card_set" - __table_args__ = ( - UniqueConstraint("name", "vendor_id"), - ) - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) - name = Column(String(255), index=True) - parallel_set = Column(BIT(1)) - insert_set = Column(BIT(1)) - vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True) - vendor = relationship("Vendor", back_populates="card_sets") - cards = relationship("Card") - - -class Card(Base): - __tablename__ = "card" - __table_args__ = ( - UniqueConstraint("card_number", "year", "vendor_id", "card_set_id"), - ) - id = Column(String, primary_key=True) - created_date = Column(DateTime) - last_modified_date = Column(DateTime) - version = Column(Integer) - card_number = Column(Integer, index=True) - year = Column(Integer, index=True) - card_set_id = Column(String, ForeignKey("card_set.id"), nullable=False) - card_set = relationship("CardSet", back_populates="cards") - rooster_id = Column(String, ForeignKey("rooster.id"), nullable=False) - rooster = relationship("Rooster", back_populates="cards") - vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False) - vendor = relationship("Vendor", back_populates="cards") diff --git a/qt/gui/data_view.py b/qt/gui/data_view.py deleted file mode 100644 index 91c66d2..0000000 --- a/qt/gui/data_view.py +++ /dev/null @@ -1,12 +0,0 @@ -from abc import ABC, abstractmethod - - -class DataViewMeta(ABC): - @abstractmethod - def get_header(self): - pass - - -class ComicView(DataViewMeta): - def get_header(self): - pass diff --git a/qt/gui/data_view_model.py b/qt/gui/data_view_model.py deleted file mode 100644 index 4ffdc8c..0000000 --- a/qt/gui/data_view_model.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import List - -from PySide6.QtCore import QModelIndex, QAbstractTableModel -from PySide6.QtGui import Qt - -from gui.data_view import DataViewMeta - - -class DataViewModel(QAbstractTableModel): - def __init__(self): - super().__init__() - self.main_window = None - self._config = None - self._data = List[DataViewMeta] - - def rowCount(self, parent=QModelIndex()): - return len(self._data) - - def columnCount(self, parent=QModelIndex()): - return 0 - - def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole): - return None - - def data(self, index, role=Qt.ItemDataRole.DisplayRole): - return None - - def setData(self, index, value, role=Qt.ItemDataRole.EditRole): - return False - - def flags(self, index): - return None diff --git a/qt/gui/dialogs.py b/qt/gui/dialogs.py deleted file mode 100644 index 21dbe92..0000000 --- a/qt/gui/dialogs.py +++ /dev/null @@ -1,106 +0,0 @@ -from pathlib import Path - -from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QFileDialog, \ - QCheckBox, QComboBox - - -class ExportKontorDialog(QDialog): - def __init__(self, parent=None, kontor_db=None): - super().__init__(parent) - - self.parent = parent - self.kontor_db = kontor_db - self.file_name = "data.json" - self.tables = [] - self._table_options = {} - - self.export_options = {"JSON": {"ext": ".json"}, "YAML": {"ext": ".yaml"}, "SQLite": {"ext": ".db"}} - self.current_export_type = "JSON" - - buttons = (QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - - self.buttonBox = QDialogButtonBox(buttons) - self.buttonBox.accepted.connect(self.accept) - self.buttonBox.rejected.connect(self.reject) - - layout = QVBoxLayout() - - self.label = QLabel() - self.label.setText("Export DB to data.json") - - self.combo_box = QComboBox() - self.combo_box.addItems(["JSON", "YAML", "SQLite"]) - self.combo_box.currentTextChanged.connect(self.change_export_type) - file_layout = QHBoxLayout() - file_layout.addWidget(self.label) - file_layout.addWidget(self.combo_box) - file_button = QPushButton("Select file") - file_button.clicked.connect(self.select_file) - file_layout.addWidget(file_button) - layout.addLayout(file_layout) - - for table_name in self.kontor_db.get_table_names(): - check_box = QCheckBox(table_name) - check_box.setChecked(True) - self.tables.append(table_name) - self._table_options[table_name] = check_box - check_box.stateChanged.connect(self.change_selection) - layout.addWidget(check_box) - layout.addWidget(self.buttonBox) - self.setLayout(layout) - - def change_selection(self): - self.tables.clear() - for (name, box) in self._table_options.items(): - if box.isChecked(): - self.tables.append(name) - - def change_export_type(self, text): - self.current_export_type = text - self.label.setText(f'Export DB to data.{self.export_options[text]["ext"]}') - - def select_file(self): - file_dialog = QFileDialog() - file_dialog.setFileMode(QFileDialog.FileMode.AnyFile) - file_dialog.setDefaultSuffix(self.export_options[self.current_export_type]["ext"]) - file_dialog.setNameFilter(f'*{self.export_options[self.current_export_type]["ext"]}') - if file_dialog.exec(): - self.file_name = file_dialog.selectedFiles()[0] - export_file = Path(self.file_name) - self.file_name = export_file.with_suffix(self.export_options[self.current_export_type]["ext"]) - self.label.setText(f"Export DB to {self.file_name}") - - def get_tables_to_export(self) -> list: - return self.tables - - -class ImportKontorDialog(QDialog): - def __init__(self, parent=None): - super().__init__(parent) - - self.file_name = None - - QBtn = (QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - - self.buttonBox = QDialogButtonBox(QBtn) - self.buttonBox.accepted.connect(self.accept) - self.buttonBox.rejected.connect(self.reject) - - self.label = QLabel() - self.label.setText("Import DB from data.json") - layout = QVBoxLayout() - file_layout = QHBoxLayout() - file_layout.addWidget(self.label) - file_button = QPushButton("Select file") - file_button.clicked.connect(self.select_file) - file_layout.addWidget(file_button) - layout.addLayout(file_layout) - layout.addWidget(self.buttonBox) - self.setLayout(layout) - - def select_file(self): - file_dialog = QFileDialog() - file_dialog.setFileMode(QFileDialog.FileMode.ExistingFile) - if file_dialog.exec(): - self.file_name = file_dialog.selectedFiles()[0] - self.label.setText(f"Import DB from {self.file_name}") diff --git a/qt/gui/main_window.py b/qt/gui/main_window.py deleted file mode 100644 index 66f2302..0000000 --- a/qt/gui/main_window.py +++ /dev/null @@ -1,137 +0,0 @@ -from PySide6.QtGui import QAction, QIcon -from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView -from PySide6.QtWidgets import QLabel, QMainWindow - -from database import KontorDB -from dialogs import ExportKontorDialog, ImportKontorDialog -from model_config import KontorModelConfig -from table_model import KontorTableModel -from media_file_model import MediaFileTableModel - - -class MainWindow(QMainWindow): - - def __init__(self, config): - super().__init__() - - self.tick = QIcon('res/tick.png') - self.cross = QIcon('res/cross.png') - self.import_icon = QIcon("res/application-import.png") - self.export_icon = QIcon("res/application-export.png") - self.circle_icon = QIcon("res/arrow-circle-double.png") - - self.setWindowTitle("Kontor") - self.setMinimumSize(800, 500) - self._create_actions() - self._create_menubar() - self._create_toolbars() - self._create_statusbar() - - self.data = [] - self.filter = {} - self.kontor_db = KontorDB(config) - self.central_widget = QWidget() - parent_layout = QVBoxLayout() - self.central_widget.setLayout(parent_layout) - self.tabs = QTabWidget() - self.tabs.addTab(self.generate_data_tab("comic", Comic), "Comics") - self.tabs.addTab(self.generate_data_tab("media_file", MediaFile), "MediaFile") - self.tabs.currentChanged.connect(self._tab_changed) - #label.setAlignment(Qt.AlignmentFlag.AlignCenter) - parent_layout.addWidget(self.tabs) - - self.setCentralWidget(self.central_widget) - - def _create_actions(self): - self.newAction = QAction("&New", self) - self.aboutAction = QAction("&Über...", self) - self.aboutAction.triggered.connect(self.about) - self.importAction = QAction(self.import_icon, "&Import", self) - self.importAction.triggered.connect(self.import_from_file) - self.exportAction = QAction(self.export_icon, "&Export", self) - self.exportAction.triggered.connect(self.export_to_file) - self.refreshAction = QAction(self.circle_icon, "&Refresh", self) - self.refreshAction.triggered.connect(self.refresh) - self.updateTitleAction = QAction("&Update Titles", self) - self.downloadAction = QAction("&Download Videos", self) - self.exitAction = QAction("&Beenden", self) - self.exitAction.setShortcut("Alt+F4") - self.exitAction.triggered.connect(self.close) - - def _create_menubar(self): - menu_bar = self.menuBar() - # File menu - file_menu = QMenu("&Datei") - menu_bar.addMenu(file_menu) - file_menu.addAction(self.exitAction) - # Kontor menu - kontor_menu = QMenu("&Kontor") - menu_bar.addMenu(kontor_menu) - kontor_menu.addAction(self.importAction) - kontor_menu.addAction(self.exportAction) - comic_menu = QMenu("&Comic") - tysc_menu = QMenu("&TradeYourSportCards") - media_file_menu = QMenu("&MediaFile") - media_file_menu.addAction(self.updateTitleAction) - media_file_menu.addAction(self.downloadAction) - kontor_menu.addMenu(comic_menu) - kontor_menu.addMenu(tysc_menu) - kontor_menu.addMenu(media_file_menu) - # Help menu - help_menu = QMenu("&Hilfe") - menu_bar.addMenu(help_menu) - help_menu.addAction(self.aboutAction) - - def _create_toolbars(self): - # Kontor toolbar - kontor_tool_bar = self.addToolBar("Kontor") - kontor_tool_bar.addAction(self.importAction) - kontor_tool_bar.addAction(self.exportAction) - kontor_tool_bar.addAction(self.refreshAction) - - def _create_statusbar(self): - self.statusBar = self.statusBar() - self.statusBar.showMessage("Kontor ready", 6000) - self.status_label = QLabel("") - self.statusBar.addPermanentWidget(self.status_label) - - def about(self): - QMessageBox.about(self.central_widget, "Über Kontor", f"Python: 3.11\nKontor: 0.1.0") - - def import_from_file(self): - import_dlg = ImportKontorDialog(self) - if import_dlg.exec(): - print(f"import DB from file {import_dlg.file_name}") - else: - print("no nothing for import") - pass - - def export_to_file(self): - export_dlg = ExportKontorDialog(self, self.kontor_db) - if export_dlg.exec(): - print(export_dlg.get_tables_to_export()) - print(f"export DB to {export_dlg.file_name}") - self.statusBar.showMessage(f"export DB to {export_dlg.file_name}", 3000) - self.kontor_db.export_db(export_dlg.current_export_type, export_dlg.file_name, export_dlg.get_tables_to_export()) - else: - self.statusBar.showMessage("Export cancelled", 3000) - - def refresh(self): - self.data[self.tabs.currentIndex()].refresh() - - def _tab_changed(self, tab_index): - self.data[tab_index].refresh() - - def generate_data_tab(self, table_name, table): - data_tab = QWidget() - table_config = KontorModelConfig(self.kontor_db, self, table_name, table) - model = KontorTableModel(table_config) - layout = QVBoxLayout() - self.data.append(model) - data_tab.setLayout(layout) - table_view = QTableView() - table_view.setModel(model) - layout.addLayout(table_config.get_filter_layout()) - layout.addWidget(table_view) - model.refresh() - return data_tab diff --git a/qt/gui/model_config.py b/qt/gui/model_config.py deleted file mode 100644 index a3f902d..0000000 --- a/qt/gui/model_config.py +++ /dev/null @@ -1,65 +0,0 @@ -import mariadb -from PySide6.QtWidgets import QHBoxLayout, QCheckBox - -from database import KontorDB - - -class KontorModelConfig: - - def __init__(self, kontor_db: KontorDB, main_window, table_name: str, table): - self.header = {} - self.filter = {} - self.main_window = main_window - self._table_name = table_name - self._table = table - self.kontor_db = kontor_db - self.get_table_config() - - def get_table_config(self): - self.header = self.kontor_db.get_column_meta_data(self._table_name) - self.filter = self.kontor_db.get_filters(self._table_name) - - def get_filter(self) -> str: - filter_rule = "" - # print(self.filter["download"].isChecked()) - for column, filter_info in self.filter.items(): - # print(column, filter_info) - if filter_info['widget'].isChecked(): - # print(column, filter_info, filter_rule, len(filter_rule)) - if len(filter_rule) < 1: - filter_rule += "WHERE " - if len(filter_rule) > 8: - filter_rule += " AND " - filter_rule += f"{column} is true" - # print(f"{filter_rule=}") - return filter_rule - - def filters(self) -> dict: - _filters = {} - # print(self.filter["download"].isChecked()) - for column, filter_info in self.filter.items(): - # print(column, filter_info) - if filter_info['widget'].isChecked(): - _filters[column] = True - # print(f"{filter_rule=}") - return _filters - - def get_data(self) -> list: - # data = self.kontor_db.get_data(self._table_name, self.header, self.get_filter()) - # data.clear() - data = self.kontor_db.data(self._table, self.header, self.filters()) - # print(f"KontorModelConfig.get_data: {len(data)}") - # comics = self.kontor_db.session.query(Comic).all() - # print(f'{len(comics)} Comics loaded') - return data - - def get_filter_layout(self) -> QHBoxLayout: - filter_layout = QHBoxLayout() - for column, filter_info in self.filter.items(): - filter_checkbox = QCheckBox() - filter_checkbox.setText(filter_info['label']) - filter_checkbox.checkStateChanged.connect(self.main_window.refresh) - self.filter[column]['widget'] = filter_checkbox - filter_layout.addWidget(filter_checkbox) - filter_layout.addStretch() - return filter_layout diff --git a/qt/gui/table_model.py b/qt/gui/table_model.py deleted file mode 100644 index e1ba6ae..0000000 --- a/qt/gui/table_model.py +++ /dev/null @@ -1,108 +0,0 @@ -from datetime import datetime - -from PySide6.QtCore import QAbstractTableModel, QModelIndex -from PySide6.QtGui import Qt - -from gui.model_config import KontorModelConfig - - -class KontorTableModel(QAbstractTableModel): - - def __init__(self, model_config: KontorModelConfig): - super().__init__() - self._main_window = model_config.main_window - self._config = model_config - self._data = [] - - def refresh(self): - data = self._config.get_data() - count = 0 - # print(data) - if data is not None: - self.beginResetModel() - self._data.clear() - self._data = data - self.endResetModel() - count = len(data) - # print(data) - # print(self._data) - self.layoutChanged.emit() - self._main_window.statusBar.showMessage(f"{count} Einträge geladen", 3000) - - def rowCount(self, parent=QModelIndex()): - # The length of the outer list. - if self._data is None: - return 0 - return len(self._data) - - def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): - if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: - return self._config.header[col]['label'] - if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: - return str(col+1) - - def data(self, index, role=Qt.ItemDataRole.DisplayRole): - if self._data is None: - return None - value = self._data[index.row()][index.column()] - # print('{}:: {}:: {}: {}'.format(index, role, value, type(value))) - if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole: - if isinstance(value, datetime): - return value.strftime("%Y-%m-%d %M:%M:%S") - if isinstance(value, str): - return value - if isinstance(value, bytes): - if value == b'\x01': - return self._main_window.tick - else: - return self._main_window.cross - if isinstance(value, int): - # print('{}:: {}: {}'.format(index, value, type(value))) - if value == 1: - return self._main_window.tick - else: - return self._main_window.cross - if isinstance(value, bool): - if value: - return self._main_window.tick - else: - return self._main_window.cross - return str(value) - if role == Qt.ItemDataRole.DecorationRole: - if isinstance(value, bytes): - if value == b'\x01': - return self._main_window.tick - else: - return self._main_window.cross - if isinstance(value, int): - if value == 1: - return self._main_window.tick - else: - return self._main_window.cross - if isinstance(value, bool): - if value: - return self._main_window.tick - else: - return self._main_window.cross - - def columnCount(self, index=QModelIndex()): - # The following takes the first sub-list, and returns - # the length (only works if all rows are an equal length) - # print(f"Header count: {len(self._config.get_header())}") - return len(self._config.header) - - def setData(self, index, value, role: int) -> bool: - print(index, role) - if role == Qt.ItemDataRole.EditRole: - self._data[index.row()][index.column()] = value - print(self._data[index.row()][index.column()]) - self.dataChanged.emit(index, index) - return True - if role == Qt.ItemDataRole.CheckStateRole: - print("role == Qt.ItemDataRole.CheckStateRole") - checked = value == Qt.CheckState.Checked - self._data[index.row()][index.column()] = checked - return False - - def flags(self, index): - return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate diff --git a/qt/kontor.py b/qt/kontor.py deleted file mode 100644 index 2eed73b..0000000 --- a/qt/kontor.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -PyQT6 GUI for Kontor -""" -import sys -from pathlib import Path -from platformdirs import PlatformDirs -from PySide6.QtWidgets import QApplication -import yaml - -from gui.main_window import MainWindow - - -if __name__ == '__main__': - app = QApplication(sys.argv) - dirs = PlatformDirs("kontor") - database_config = Path(dirs.user_config_dir, 'database-config.yaml') - with open(database_config, 'rt') as f: - db_config = yaml.safe_load(f.read()) - window = MainWindow(db_config) - window.show() - app.exec() diff --git a/qt/pysidedeploy.spec b/qt/pysidedeploy.spec deleted file mode 100644 index 8e65745..0000000 --- a/qt/pysidedeploy.spec +++ /dev/null @@ -1,98 +0,0 @@ -[app] - -# title of your application -title = kontor - -# project directory. the general assumption is that project_dir is the parent directory -# of input_file -project_dir = /home/tpeetz/projects/kontor/qt - -# source file path -input_file = /home/tpeetz/projects/kontor/qt/kontor.py - -# directory where exec is stored -exec_directory = . - -# path to .pyproject project file -project_file = - -# application icon -icon = /usr/local/lib/python3.11/dist-packages/PySide6/scripts/deploy_lib/pyside_icon.jpg - -[python] - -# python path -python_path = /usr/bin/python3 - -# python packages to install -packages = Nuitka==2.4.8 - -# buildozer = for deploying Android application -android_packages = buildozer==1.5.0,cython==0.29.33 - -[qt] - -# comma separated path to qml files required -# normally all the qml files required by the project are added automatically -qml_files = - -# excluded qml plugin binaries -excluded_qml_plugins = - -# qt modules used. comma separated -modules = Gui,DBus,Core,Widgets - -# qt plugins used by the application -plugins = platformthemes,imageformats,platforms,generic,iconengines,egldeviceintegrations,styles,xcbglintegrations,platforms/darwin,platforminputcontexts,accessiblebridge - -[android] - -# path to pyside wheel -wheel_pyside = - -# path to shiboken wheel -wheel_shiboken = - -# plugins to be copied to libs folder of the packaged application. comma separated -plugins = - -[nuitka] - -# usage description for permissions requested by the app as found in the info.plist file -# of the app bundle -# eg = extra_args = --show-modules --follow-stdlib -macos.permissions = - -# mode of using nuitka. accepts standalone or onefile. default is onefile. -mode = onefile - -# (str) specify any extra nuitka arguments -extra_args = --quiet --noinclude-qt-translations - -[buildozer] - -# build mode -# possible options = [release, debug] -# release creates an aab, while debug creates an apk -mode = debug - -# contrains path to pyside6 and shiboken6 recipe dir -recipe_dir = - -# path to extra qt android jars to be loaded by the application -jars_dir = - -# if empty uses default ndk path downloaded by buildozer -ndk_path = - -# if empty uses default sdk path downloaded by buildozer -sdk_path = - -# other libraries to be loaded. comma separated. -# loaded at app startup -local_libs = - -# architecture of deployed platform -# possible values = ["aarch64", "armv7a", "i686", "x86_64"] -arch = - diff --git a/qt/res/application-export.png b/qt/res/application-export.png deleted file mode 100644 index 555887a28d64bc812c4dfa98a6ff1da1927b7792..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 513 zcmV+c0{;DpP)A0|fCeqz@KJVX8nnt2D9k2UrOajFq_SwR4OUbbUJ;HgeDQF66oE777mBiWdd2F(NKOO zk$Ax17hTui{#6jPXfz7dY87I!*bgRr4TVA=afmd=7~nkYBq$b(su#^>Q?2y;%rJ~= zA;j4sgM^B|5YOb(M4cc`5q!^h_wPg5is0Dq{42l!6{r`}{QE*r00000NkvXXu0mjf Djqud07&@TR)18#v5Hqj8@|Bnpn>z;V`CueWd< z7oI05i2^A#D9Qz9v#hs`;>*D7{eB7>0ppH2SLstJO-8BwR~M`A7kF ztyTlI66tgrtyT-MSPa~yU>3AuKR54&OXJih@;oco-=1sDLQa}`@LC7Iy> O0000dz zNLw#yfl;S$OS+NxP!T0`q27a_KcR=-dJ5{Xm+WEf!J>k|BqE5#C}ImGwQ$$<(wdh& z>AIJ5zVFx9lo8J{@aWY%oIP6CAZKYHol)uRf|554m_YTnpAlGl zL9{5M4)qVt#u>|mLQVrh46q|<)MP|~t8EE-8crSv>~io{4+abW2A~lV0z>OM)mbBD zmWtqV;+?9YGlhpQQ@M9zi{*KRG_T5PQ_nvC9tMo_mT6XEIU5;J+S?d(HD_0f!1ES7 z7@Uj?4!kD>&?r8e&c1jan^9Cs!}3z0d7Izafj)-}rbYZR}EdihS6`M#Fcy= zaw{u#mV;BNWe3{Sqiq;{_M>cOP*8MhP1CLQf3t|C2tpM55ycYh7RL%}Yf+Bp#e!~C z2(WG^ZnlxC>igv!sW2GM8SslVJ(oFwkp~5*6E^0rT$Q zya}^2lB>ITrX0zGEa!lc90$HC4~#^g#%ZB2j3vzHhi;TO$n{?Wwx(^0>^QA_x~H^z zTf=2SxY0rCf|fl@rb7TXYr%pE(0=u@G diff --git a/qt/res/cross.png b/qt/res/cross.png deleted file mode 100644 index 6b9fa6dd36ee8165272a13dd263f573507c78ca6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544 zcmV+*0^j|KP)L-ku(! z6_?D-?!0+#=VtbVZQGdTnZt~apI_HPK#=zVadHM((E`e*C+RoFb)Qo8-U{Lb7{`Tz zw4B8FZ|o?Wox+p=1wp47C;7ar*Xu~;a?<=sjPv>+otDjJ6FaGt!YnNyxQQhp)G1V! zk;r6ZtyV)c8pTbiRAnHRNXTxti%=+p$4aG2*+mMM&xrdiU^`VPk^N*+HX98^7!HT% zwA%;A{T)GxeQ|RcxyzV%! z$AbYD+)i1R64zB?q}NmTz}5|04~J#YG_goA*Eq(QJvp7pG14hUj1pZ^t>3S*xqHUO ze~r;}zTY_XkZ*}d@gm!;N90h8nBQenBUZ_ukZKNicn*hc_Pmc!Jn|2=s<~}>!Sb>Qm3!QP46b_JFw5YU70Tu$X}=T}ez@@t%j iG9vD)nDux55?}x$+|UyQVK_bj0000tYd4K$mX5uyr2F@fdffp{&DSHtl4|Bn9=ukpCx`#%PTUr-D(@b7;C zhJXJjrnw{;1KBM=6&|E`fsNrGL!Y6%zUh}QUl`(@V)PmQFtotEKmafTHP0uP}7&%m4pcKQ#X)BiAJ2y+PrtBI>9eEIt2-_c7)?*Lsf z5vX5*Af@m-wBJRV%#Fiz=BdPr0!2^a2d-yv7gSU);Z6{`~Oy?E5qSnHUln4*Y%!){F!Y1~5WXoC44g zb7l_)X~q^V6MmWR7e3wj|L|Wb!|^}Y86N$^h+kv_Kw-fP!~#If$6(B4$)LlK#&G6; zC&ShMSAb$5tA9Xg5dOsg@-z^@3;;QS9f&!gG&3|;{Da~@Q2ZB({s%XJ5&#fj0J|In U+D>(nEdT%j07*qoM6N<$g0@&8X#fBK From 60fba0d3e9751c17ed31e8ede8f779caf4c6429a Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 19 Jan 2025 23:39:53 +0100 Subject: [PATCH 57/91] add dependencies to kontor-schema and kontor-video in rquirements.txt --- python/kontor-cli/requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/kontor-cli/requirements.txt b/python/kontor-cli/requirements.txt index 5b4c054..92890d6 100644 --- a/python/kontor-cli/requirements.txt +++ b/python/kontor-cli/requirements.txt @@ -1,7 +1,9 @@ +-e /home/tpeetz/projects/kontor/python/kontor-schema +-e /home/tpeetz/projects/kontor/python/kontor-video + cement==3.0.12 cement[jinja2] cement[yaml] cement[colorlog] mariadb sqlalchemy - From 65288a53a13afb6e4fc78772624a20691f8c53c5 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 20 Jan 2025 15:40:45 +0100 Subject: [PATCH 58/91] complete MetaDataTable and MetaDataColumn --- .../kontor-cli/kontor/controllers/database.py | 42 ++++- python/kontor-cli/kontor/main.py | 4 +- python/kontor-cli/requirements.txt | 4 +- python/kontor-gui/.gitignore | 1 + python/kontor-gui/gui/main_window.py | 4 +- python/kontor-gui/requirements.txt | 4 +- .../site/python3.11/greenlet/greenlet.h | 164 ------------------ .../kontor-schema/kontor_schema/__init__.py | 37 +++- python/kontor-schema/kontor_schema/admin.py | 78 +++++++++ python/kontor-schema/kontor_schema/base.py | 2 +- .../kontor-schema/kontor_schema/metadata.py | 6 +- .../kontor/admin/SetupModuleAdmin.java | 84 +++++++++ .../kontor/admin/data/MetaDataColumn.java | 2 +- .../de/thpeetz/kontor/admin/data/Token.java | 2 + .../admin/services/MetaDataService.java | 2 +- .../thpeetz/kontor/media/data/MediaLink.java | 40 ----- 16 files changed, 257 insertions(+), 219 deletions(-) delete mode 100644 python/kontor-schema/include/site/python3.11/greenlet/greenlet.h create mode 100644 python/kontor-schema/kontor_schema/admin.py delete mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/data/MediaLink.java diff --git a/python/kontor-cli/kontor/controllers/database.py b/python/kontor-cli/kontor/controllers/database.py index 73732f4..a289c99 100644 --- a/python/kontor-cli/kontor/controllers/database.py +++ b/python/kontor-cli/kontor/controllers/database.py @@ -1,9 +1,9 @@ +import mariadb from cement import Controller, ex from kontor_schema import KontorDB class Database(Controller): - class Meta: label = 'database' stacked_type = 'nested' @@ -49,3 +49,43 @@ class Database(Controller): kontor_db = KontorDB(self.app.engine, self.app.config, self.app.log) self.app.render(data, 'import.jinja2') kontor_db.import_db(data['db_file'], self.app.pargs.dry_run) + + @ex( + help='check the db schema against MetaDataTable and MetaDataColumn' + ) + def check(self): + mariadb_conn = mariadb.connect( + host=self.app.config['mariadb']['host'], + port=int(self.app.config['mariadb']['port']), + user=self.app.config['mariadb']['user'], + password=self.app.config['mariadb']['password'], + database=self.app.config['mariadb']['database'] + ) + table_list = [] + cursor = mariadb_conn.cursor() + cursor.execute("SHOW TABLES") + for (tablename,) in cursor.fetchall(): + table_list.append(tablename) + kontor_db = KontorDB(self.app.engine, self.app.log) + table_names = kontor_db.get_table_names() + for table in table_list: + if table not in table_names: + self.app.log.info(f"{table} is not stored in MetaDataTable") + continue + meta_data = kontor_db.get_columns(table) + field_info = self.get_table_field_info(cursor, table) + for column in field_info: + if column not in meta_data: + self.app.log.info(f"column {column} of table {table} is not in MetaDataColumn") + mariadb_conn.close() + + def get_table_field_info(self, cursor, table) -> dict: + info = {} + cursor.execute(f"SELECT * FROM {table} LIMIT 1") + field_info = mariadb.fieldinfo() + for column in cursor.description: + column_name = column[0] + column_type = field_info.type(column) + column_flags = field_info.flag(column) + info[column_name] = {"type": column_type, "flags": column_flags} + return info diff --git a/python/kontor-cli/kontor/main.py b/python/kontor-cli/kontor/main.py index 8884b87..2d71a3e 100644 --- a/python/kontor-cli/kontor/main.py +++ b/python/kontor-cli/kontor/main.py @@ -12,7 +12,6 @@ from .controllers.media import Media # configuration defaults CONFIG = init_defaults('kontor', 'mariadb', 'media') -CONFIG['kontor']['foo'] = 'bar' CONFIG['mariadb']['user'] = 'kontor' CONFIG['mariadb']['password'] = 'kontor' CONFIG['mariadb']['host'] = '127.0.0.1' @@ -21,8 +20,9 @@ CONFIG['mariadb']['database'] = 'kontor' CONFIG['media']['yt-dlp'] = '/home/tpeetz/bin/yt-dlp' CONFIG['media']['dir'] = '/data/media' + def extend_sqlalchemy(app): - app.log.info('extending kontor application with sqlalchemy') + app.log.debug('extending kontor application with sqlalchemy') connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format( app.config.get('mariadb', 'user'), app.config.get('mariadb', 'password'), diff --git a/python/kontor-cli/requirements.txt b/python/kontor-cli/requirements.txt index 92890d6..65cbadd 100644 --- a/python/kontor-cli/requirements.txt +++ b/python/kontor-cli/requirements.txt @@ -1,5 +1,5 @@ --e /home/tpeetz/projects/kontor/python/kontor-schema --e /home/tpeetz/projects/kontor/python/kontor-video +-e ../kontor-schema +-e ../kontor-video cement==3.0.12 cement[jinja2] diff --git a/python/kontor-gui/.gitignore b/python/kontor-gui/.gitignore index 8d5ef37..9c0554f 100644 --- a/python/kontor-gui/.gitignore +++ b/python/kontor-gui/.gitignore @@ -1,4 +1,5 @@ deployment/ +venv/ kontor.bin bin/ include/ diff --git a/python/kontor-gui/gui/main_window.py b/python/kontor-gui/gui/main_window.py index 4f4ee91..739212e 100644 --- a/python/kontor-gui/gui/main_window.py +++ b/python/kontor-gui/gui/main_window.py @@ -1,4 +1,4 @@ -from PySide6.QtGui import QAction, QIcon +from PySide6.QtGui import QAction, QIcon, QGuiApplication from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView, QProgressBar from PySide6.QtWidgets import QLabel, QMainWindow from sqlalchemy import Engine @@ -45,6 +45,8 @@ class MainWindow(QMainWindow): parent_layout.addWidget(self.tabs) self.setCentralWidget(self.central_widget) + centerPoint = QGuiApplication.screens()[0].geometry().center() + self.move(centerPoint - self.frameGeometry().center()) def _create_actions(self): self.newAction = QAction("&New", self) diff --git a/python/kontor-gui/requirements.txt b/python/kontor-gui/requirements.txt index 90b0945..057d486 100644 --- a/python/kontor-gui/requirements.txt +++ b/python/kontor-gui/requirements.txt @@ -1,5 +1,5 @@ --e /home/tpeetz/projects/kontor/python/kontor-schema --e /home/tpeetz/projects/kontor/python/kontor-video +-e ../kontor-schema +-e ../kontor-video platformdirs pyyaml diff --git a/python/kontor-schema/include/site/python3.11/greenlet/greenlet.h b/python/kontor-schema/include/site/python3.11/greenlet/greenlet.h deleted file mode 100644 index d02a16e..0000000 --- a/python/kontor-schema/include/site/python3.11/greenlet/greenlet.h +++ /dev/null @@ -1,164 +0,0 @@ -/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ - -/* Greenlet object interface */ - -#ifndef Py_GREENLETOBJECT_H -#define Py_GREENLETOBJECT_H - - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* This is deprecated and undocumented. It does not change. */ -#define GREENLET_VERSION "1.0.0" - -#ifndef GREENLET_MODULE -#define implementation_ptr_t void* -#endif - -typedef struct _greenlet { - PyObject_HEAD - PyObject* weakreflist; - PyObject* dict; - implementation_ptr_t pimpl; -} PyGreenlet; - -#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type)) - - -/* C API functions */ - -/* Total number of symbols that are exported */ -#define PyGreenlet_API_pointers 12 - -#define PyGreenlet_Type_NUM 0 -#define PyExc_GreenletError_NUM 1 -#define PyExc_GreenletExit_NUM 2 - -#define PyGreenlet_New_NUM 3 -#define PyGreenlet_GetCurrent_NUM 4 -#define PyGreenlet_Throw_NUM 5 -#define PyGreenlet_Switch_NUM 6 -#define PyGreenlet_SetParent_NUM 7 - -#define PyGreenlet_MAIN_NUM 8 -#define PyGreenlet_STARTED_NUM 9 -#define PyGreenlet_ACTIVE_NUM 10 -#define PyGreenlet_GET_PARENT_NUM 11 - -#ifndef GREENLET_MODULE -/* This section is used by modules that uses the greenlet C API */ -static void** _PyGreenlet_API = NULL; - -# define PyGreenlet_Type \ - (*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM]) - -# define PyExc_GreenletError \ - ((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM]) - -# define PyExc_GreenletExit \ - ((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM]) - -/* - * PyGreenlet_New(PyObject *args) - * - * greenlet.greenlet(run, parent=None) - */ -# define PyGreenlet_New \ - (*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \ - _PyGreenlet_API[PyGreenlet_New_NUM]) - -/* - * PyGreenlet_GetCurrent(void) - * - * greenlet.getcurrent() - */ -# define PyGreenlet_GetCurrent \ - (*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM]) - -/* - * PyGreenlet_Throw( - * PyGreenlet *greenlet, - * PyObject *typ, - * PyObject *val, - * PyObject *tb) - * - * g.throw(...) - */ -# define PyGreenlet_Throw \ - (*(PyObject * (*)(PyGreenlet * self, \ - PyObject * typ, \ - PyObject * val, \ - PyObject * tb)) \ - _PyGreenlet_API[PyGreenlet_Throw_NUM]) - -/* - * PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args) - * - * g.switch(*args, **kwargs) - */ -# define PyGreenlet_Switch \ - (*(PyObject * \ - (*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \ - _PyGreenlet_API[PyGreenlet_Switch_NUM]) - -/* - * PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent) - * - * g.parent = new_parent - */ -# define PyGreenlet_SetParent \ - (*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \ - _PyGreenlet_API[PyGreenlet_SetParent_NUM]) - -/* - * PyGreenlet_GetParent(PyObject* greenlet) - * - * return greenlet.parent; - * - * This could return NULL even if there is no exception active. - * If it does not return NULL, you are responsible for decrementing the - * reference count. - */ -# define PyGreenlet_GetParent \ - (*(PyGreenlet* (*)(PyGreenlet*)) \ - _PyGreenlet_API[PyGreenlet_GET_PARENT_NUM]) - -/* - * deprecated, undocumented alias. - */ -# define PyGreenlet_GET_PARENT PyGreenlet_GetParent - -# define PyGreenlet_MAIN \ - (*(int (*)(PyGreenlet*)) \ - _PyGreenlet_API[PyGreenlet_MAIN_NUM]) - -# define PyGreenlet_STARTED \ - (*(int (*)(PyGreenlet*)) \ - _PyGreenlet_API[PyGreenlet_STARTED_NUM]) - -# define PyGreenlet_ACTIVE \ - (*(int (*)(PyGreenlet*)) \ - _PyGreenlet_API[PyGreenlet_ACTIVE_NUM]) - - - - -/* Macro that imports greenlet and initializes C API */ -/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we - keep the older definition to be sure older code that might have a copy of - the header still works. */ -# define PyGreenlet_Import() \ - { \ - _PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \ - } - -#endif /* GREENLET_MODULE */ - -#ifdef __cplusplus -} -#endif -#endif /* !Py_GREENLETOBJECT_H */ diff --git a/python/kontor-schema/kontor_schema/__init__.py b/python/kontor-schema/kontor_schema/__init__.py index 3549955..a70aa6d 100644 --- a/python/kontor-schema/kontor_schema/__init__.py +++ b/python/kontor-schema/kontor_schema/__init__.py @@ -5,10 +5,12 @@ import uuid from datetime import datetime from pathlib import Path +import requests from sqlalchemy import Engine from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import sessionmaker +from .admin import User, Token, Role, AuthorizationMatrix, ModuleData, MailAccount, Mail from .bookshelf import Article, Book, Author, BookshelfPublisher, ArticleAuthor, BookAuthor from .comic import Comic, Artist, Publisher, Issue, StoryArc, TradePaperback, Volume, ComicWork, WorkType from .metadata import MetaDataTable, MetaDataColumn @@ -51,8 +53,15 @@ class KontorDB: self.registry['media_file'] = MediaFile self.registry['media_article'] = MediaArticle self.registry['media_video'] = MediaVideo - self.registry['meta_data_table'] = MetaDataTable + self.registry[MetaDataTable.__tablename__] = MetaDataTable self.registry[MetaDataColumn.__tablename__] = MetaDataColumn + self.registry[User.__tablename__] = User + self.registry[Token.__tablename__] = Token + self.registry[Role.__tablename__] = Role + self.registry[AuthorizationMatrix.__tablename__] = AuthorizationMatrix + self.registry[ModuleData.__tablename__] = ModuleData + self.registry[MailAccount.__tablename__] = MailAccount + self.registry[Mail.__tablename__] = Mail def get_table_names(self) -> list: result = [] @@ -87,6 +96,17 @@ class KontorDB: order += 1 return meta_data + def get_columns(self, table_name: str) -> dict: + columns = {} + order = 0 + __session__ = sessionmaker(self.engine) + with __session__() as session: + for (_, column) in (session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name).all()): + columns[column.column_name] = {"order": column.column_order, "type": column.column_type} + return columns + def get_filters(self, table_name): _filter_map = {} __session__ = sessionmaker(self.engine) @@ -300,6 +320,21 @@ class KontorDB: link.review = 0 session.commit() + def get_update_list(self) -> list[str]: + self.log.debug("get links marked as review") + update_list = [] + __session__ = sessionmaker(self.engine) + with __session__() as session: + links = session.query(MediaFile).filter(MediaFile.review == 1).all() + for link in links: + url = link.url + if url is None: + self.log.info(f"url has not been set for {link.id}") + continue + update_list.append(url) + self.log.debug(f"found {len(update_list)} urls for updates") + return update_list + def get_download_list(self) -> list[str]: self.log.debug("get links marked as should_download") download_list = [] diff --git a/python/kontor-schema/kontor_schema/admin.py b/python/kontor-schema/kontor_schema/admin.py new file mode 100644 index 0000000..b846d97 --- /dev/null +++ b/python/kontor-schema/kontor_schema/admin.py @@ -0,0 +1,78 @@ +from datetime import datetime + +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship, mapped_column, Mapped + +from .base import Base, BaseMixin + + +class User(Base, BaseMixin): + __tablename__ = 'user' + first_name = Column(String(255)) + last_name = Column(String(255)) + user_name = Column(String(255), nullable=False) + email = Column(String(255)) + password = Column(String(255)) + enabled = Column(BIT(1)) + matrix = relationship("AuthorizationMatrix") + tokens = relationship("Token") + + def get_full_name(self) -> str: + full_name = "" + if self.first_name is not None: + full_name += self.first_name + if self.last_name is not None: + if len(full_name) > 0: + full_name += " " + full_name += self.last_name + return full_name + + +class Token(Base, BaseMixin): + __tablename__ = "token" + token = Column(String(255), nullable=False, unique=True) + name = Column(String(255)) + last_used_date: Mapped[datetime] = mapped_column() + enabled = Column(BIT(1)) + user_id = Column(String, ForeignKey("user.id"), nullable=False) + user = relationship("User", back_populates="tokens") + + +class Role(Base, BaseMixin): + __tablename__ = "role" + name = Column(String(255), nullable=False) + matrix = relationship("AuthorizationMatrix") + + +class AuthorizationMatrix(Base, BaseMixin): + __tablename__ = "authorization_matrix" + user_id = Column(String, ForeignKey("user.id"), nullable=False) + user = relationship("User", back_populates="matrix") + role_id = Column(String, ForeignKey("role.id"), nullable=False) + role = relationship("Role", back_populates="matrix") + + +class ModuleData(Base, BaseMixin): + __tablename__ = "module_data" + module_name = Column(String(255), nullable=False) + import_data = Column(BIT(1)) + + +class MailAccount(Base, BaseMixin): + __tablename__ = "mail_account" + host = Column(String(255)) + port = Column(Integer) + protocol = Column(String(255)) + user_name = Column(String(255)) + password = Column(String(255)) + start_tls = Column(BIT(1)) + + +class Mail(Base, BaseMixin): + __tablename__ = "mail" + folder: Mapped[str] = mapped_column() + subject: Mapped[str] = mapped_column() + body: Mapped[str] = mapped_column() + sent_date: Mapped[datetime] = mapped_column() + received_date: Mapped[datetime] = mapped_column() diff --git a/python/kontor-schema/kontor_schema/base.py b/python/kontor-schema/kontor_schema/base.py index c976167..21186d4 100644 --- a/python/kontor-schema/kontor_schema/base.py +++ b/python/kontor-schema/kontor_schema/base.py @@ -1,7 +1,7 @@ import uuid from datetime import datetime -from sqlalchemy import Integer, func, String +from sqlalchemy import func from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column diff --git a/python/kontor-schema/kontor_schema/metadata.py b/python/kontor-schema/kontor_schema/metadata.py index 21d4d49..f9538bb 100644 --- a/python/kontor-schema/kontor_schema/metadata.py +++ b/python/kontor-schema/kontor_schema/metadata.py @@ -19,11 +19,11 @@ class MetaDataTable(Base, BaseMixin): class MetaDataColumn(Base, BaseMixin): __tablename__ = 'meta_data_column' - column_modifier = Column(String(255), nullable=True) - column_name = Column(String(255)) - column_order = Column(Integer) + column_name = Column(String(255), nullable=False) column_sync_name = Column(String(255)) column_type = Column(String(255)) + column_modifier = Column(String(255), nullable=True) + column_order = Column(Integer) table_id = Column(String, ForeignKey('meta_data_table.id')) table = relationship("MetaDataTable", back_populates="table_columns") column_label = Column(String(255)) diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java index b54f74e..3f12472 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java @@ -332,5 +332,89 @@ public class SetupModuleAdmin implements ApplicationListener Date: Tue, 21 Jan 2025 09:42:33 +0100 Subject: [PATCH 59/91] remove kontor-cli.backup --- python/kontor-cli.backup/.gitignore | 105 -------- python/kontor-cli.backup/CHANGELOG.md | 5 - python/kontor-cli.backup/Dockerfile | 10 - python/kontor-cli.backup/LICENSE.md | 1 - python/kontor-cli.backup/MANIFEST.in | 5 - python/kontor-cli.backup/Makefile | 31 --- python/kontor-cli.backup/README.md | 69 ------ .../config/kontor.yml.example | 46 ---- python/kontor-cli.backup/docs/.gitkeep | 0 python/kontor-cli.backup/kontor/__init__.py | 0 .../kontor/controllers/__init__.py | 0 .../kontor/controllers/base.py | 35 --- .../kontor/controllers/database.py | 51 ---- .../kontor/controllers/media.py | 79 ------ .../kontor-cli.backup/kontor/core/__init__.py | 0 python/kontor-cli.backup/kontor/core/exc.py | 4 - .../kontor-cli.backup/kontor/core/version.py | 7 - .../kontor-cli.backup/kontor/ext/__init__.py | 0 python/kontor-cli.backup/kontor/main.py | 124 ---------- .../kontor/plugins/__init__.py | 0 .../kontor/templates/__init__.py | 0 .../kontor/templates/command1.jinja2 | 4 - .../kontor/templates/download.jinja2 | 2 - .../kontor/templates/import.jinja2 | 2 - .../kontor/templates/update.jinja2 | 2 - python/kontor-cli.backup/requirements-dev.txt | 8 - python/kontor-cli.backup/requirements.txt | 6 - python/kontor-cli.backup/setup.cfg | 0 python/kontor-cli.backup/setup.py | 28 --- python/kontor-cli.backup/tests/conftest.py | 16 -- python/kontor-cli.backup/tests/test_kontor.py | 36 --- python/kontor-cli/kontor/controllers/base.py | 26 -- .../kontor-cli/kontor/controllers/database.py | 18 +- python/kontor-cli/kontor/controllers/media.py | 46 +++- .../kontor/templates/command1.jinja2 | 4 - .../kontor/templates/export_db.jinja2 | 5 + python/kontor-cli/pyproject.toml | 29 +++ python/kontor-cli/requirements.txt | 1 + .../kontor-schema/kontor_schema/__init__.py | 228 +++++------------- python/kontor-video/kontor_video/__init__.py | 52 +++- 40 files changed, 196 insertions(+), 889 deletions(-) delete mode 100644 python/kontor-cli.backup/.gitignore delete mode 100644 python/kontor-cli.backup/CHANGELOG.md delete mode 100644 python/kontor-cli.backup/Dockerfile delete mode 100644 python/kontor-cli.backup/LICENSE.md delete mode 100644 python/kontor-cli.backup/MANIFEST.in delete mode 100644 python/kontor-cli.backup/Makefile delete mode 100644 python/kontor-cli.backup/README.md delete mode 100644 python/kontor-cli.backup/config/kontor.yml.example delete mode 100644 python/kontor-cli.backup/docs/.gitkeep delete mode 100644 python/kontor-cli.backup/kontor/__init__.py delete mode 100644 python/kontor-cli.backup/kontor/controllers/__init__.py delete mode 100644 python/kontor-cli.backup/kontor/controllers/base.py delete mode 100644 python/kontor-cli.backup/kontor/controllers/database.py delete mode 100644 python/kontor-cli.backup/kontor/controllers/media.py delete mode 100644 python/kontor-cli.backup/kontor/core/__init__.py delete mode 100644 python/kontor-cli.backup/kontor/core/exc.py delete mode 100644 python/kontor-cli.backup/kontor/core/version.py delete mode 100644 python/kontor-cli.backup/kontor/ext/__init__.py delete mode 100644 python/kontor-cli.backup/kontor/main.py delete mode 100644 python/kontor-cli.backup/kontor/plugins/__init__.py delete mode 100644 python/kontor-cli.backup/kontor/templates/__init__.py delete mode 100644 python/kontor-cli.backup/kontor/templates/command1.jinja2 delete mode 100644 python/kontor-cli.backup/kontor/templates/download.jinja2 delete mode 100644 python/kontor-cli.backup/kontor/templates/import.jinja2 delete mode 100644 python/kontor-cli.backup/kontor/templates/update.jinja2 delete mode 100644 python/kontor-cli.backup/requirements-dev.txt delete mode 100644 python/kontor-cli.backup/requirements.txt delete mode 100644 python/kontor-cli.backup/setup.cfg delete mode 100644 python/kontor-cli.backup/setup.py delete mode 100644 python/kontor-cli.backup/tests/conftest.py delete mode 100644 python/kontor-cli.backup/tests/test_kontor.py delete mode 100644 python/kontor-cli/kontor/templates/command1.jinja2 create mode 100644 python/kontor-cli/kontor/templates/export_db.jinja2 create mode 100644 python/kontor-cli/pyproject.toml diff --git a/python/kontor-cli.backup/.gitignore b/python/kontor-cli.backup/.gitignore deleted file mode 100644 index a74b246..0000000 --- a/python/kontor-cli.backup/.gitignore +++ /dev/null @@ -1,105 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -coverage-report/ -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ diff --git a/python/kontor-cli.backup/CHANGELOG.md b/python/kontor-cli.backup/CHANGELOG.md deleted file mode 100644 index 10e1f8a..0000000 --- a/python/kontor-cli.backup/CHANGELOG.md +++ /dev/null @@ -1,5 +0,0 @@ -# Kontor CLI Change History - -## 0.0.1 - -Initial release. diff --git a/python/kontor-cli.backup/Dockerfile b/python/kontor-cli.backup/Dockerfile deleted file mode 100644 index d32cf5c..0000000 --- a/python/kontor-cli.backup/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM python:3.9-alpine -LABEL MAINTAINER="Thomas Peetz " -ENV PS1="\[\e[0;33m\]|> kontor <| \[\e[1;35m\]\W\[\e[0m\] \[\e[0m\]# " - -WORKDIR /src -COPY . /src -RUN pip install --no-cache-dir -r requirements.txt \ - && python setup.py install -WORKDIR / -ENTRYPOINT ["kontor"] diff --git a/python/kontor-cli.backup/LICENSE.md b/python/kontor-cli.backup/LICENSE.md deleted file mode 100644 index 8b13789..0000000 --- a/python/kontor-cli.backup/LICENSE.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/python/kontor-cli.backup/MANIFEST.in b/python/kontor-cli.backup/MANIFEST.in deleted file mode 100644 index 1160952..0000000 --- a/python/kontor-cli.backup/MANIFEST.in +++ /dev/null @@ -1,5 +0,0 @@ -recursive-include *.py -include setup.cfg -include README.md CHANGELOG.md LICENSE.md -include *.txt -recursive-include kontor/templates * diff --git a/python/kontor-cli.backup/Makefile b/python/kontor-cli.backup/Makefile deleted file mode 100644 index b016c3c..0000000 --- a/python/kontor-cli.backup/Makefile +++ /dev/null @@ -1,31 +0,0 @@ -.PHONY: clean virtualenv test docker dist dist-upload - -clean: - find . -name '*.py[co]' -delete - -virtualenv: - virtualenv --prompt '|> kontor <| ' env - env/bin/pip install -r requirements-dev.txt - env/bin/python setup.py develop - @echo - @echo "VirtualENV Setup Complete. Now run: source env/bin/activate" - @echo - -test: - python -m pytest \ - -v \ - --cov=kontor \ - --cov-report=term \ - --cov-report=html:coverage-report \ - tests/ - -docker: clean - docker build -t kontor:latest . - -dist: clean - rm -rf dist/* - python setup.py sdist - python setup.py bdist_wheel - -dist-upload: - twine upload dist/* diff --git a/python/kontor-cli.backup/README.md b/python/kontor-cli.backup/README.md deleted file mode 100644 index a95f4ea..0000000 --- a/python/kontor-cli.backup/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# Kontor CLI - -## Installation - -``` -$ pip install -r requirements.txt - -$ python setup.py install -``` - -## Development - -This project includes a number of helpers in the `Makefile` to streamline common development tasks. - -### Environment Setup - -The following demonstrates setting up and working with a development environment: - -``` -### create a virtualenv for development - -$ make virtualenv - -$ source env/bin/activate - - -### run kontor cli application - -$ kontor --help - - -### run pytest / coverage - -$ make test -``` - - -### Releasing to PyPi - -Before releasing to PyPi, you must configure your login credentials: - -**~/.pypirc**: - -``` -[pypi] -username = YOUR_USERNAME -password = YOUR_PASSWORD -``` - -Then use the included helper function via the `Makefile`: - -``` -$ make dist - -$ make dist-upload -``` - -## Deployments - -### Docker - -Included is a basic `Dockerfile` for building and distributing `Kontor CLI`, -and can be built with the included `make` helper: - -``` -$ make docker - -$ docker run -it kontor --help -``` diff --git a/python/kontor-cli.backup/config/kontor.yml.example b/python/kontor-cli.backup/config/kontor.yml.example deleted file mode 100644 index 6806151..0000000 --- a/python/kontor-cli.backup/config/kontor.yml.example +++ /dev/null @@ -1,46 +0,0 @@ -### Kontor CLI Configuration Settings ---- - -kontor: - -### Toggle application level debug (does not toggle framework debugging) -# debug: false - -### Where external (third-party) plugins are loaded from -# plugin_dir: /var/lib/kontor/plugins/ - -### Where all plugin configurations are loaded from -# plugin_config_dir: /etc/kontor/plugins.d/ - -### Where external templates are loaded from -# template_dir: /var/lib/kontor/templates/ - -### The log handler label -# log_handler: colorlog - -### The output handler label -# output_handler: jinja2 - -### sample foo option -# foo: bar - - -log.colorlog: - -### Where the log file lives (no log file by default) -# file: null - -### The level for which to log. One of: info, warning, error, fatal, debug -# level: info - -### Whether or not to log to console -# to_console: true - -### Whether or not to rotate the log file when it reaches `max_bytes` -# rotate: false - -### Max size in bytes that a log file can grow until it is rotated. -# max_bytes: 512000 - -### The maximum number of log files to maintain when rotating -# max_files: 4 diff --git a/python/kontor-cli.backup/docs/.gitkeep b/python/kontor-cli.backup/docs/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/python/kontor-cli.backup/kontor/__init__.py b/python/kontor-cli.backup/kontor/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/python/kontor-cli.backup/kontor/controllers/__init__.py b/python/kontor-cli.backup/kontor/controllers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/python/kontor-cli.backup/kontor/controllers/base.py b/python/kontor-cli.backup/kontor/controllers/base.py deleted file mode 100644 index 90153a3..0000000 --- a/python/kontor-cli.backup/kontor/controllers/base.py +++ /dev/null @@ -1,35 +0,0 @@ - -from cement import Controller, ex -from cement.utils.version import get_version_banner -from ..core.version import get_version - -VERSION_BANNER = """ -Kontor CLI %s -%s -""" % (get_version(), get_version_banner()) - - -class Base(Controller): - class Meta: - label = 'base' - - # text displayed at the top of --help output - description = 'Kontor CLI Tool' - - # text displayed at the bottom of --help output - epilog = 'Usage: kontor (database | media) [subcommands]' - - # controller level arguments. ex: 'kontor --version' - arguments = [ - ### add a version banner - (['-v', '--version'], - {'action': 'version', - 'version': VERSION_BANNER}), - (['-m', '--dry-run'], - {'action': 'store_true', - 'dest': 'dry_run'}) - ] - - def _default(self): - """Default action if no sub-command is passed.""" - self.app.args.print_help() diff --git a/python/kontor-cli.backup/kontor/controllers/database.py b/python/kontor-cli.backup/kontor/controllers/database.py deleted file mode 100644 index 5917d0a..0000000 --- a/python/kontor-cli.backup/kontor/controllers/database.py +++ /dev/null @@ -1,51 +0,0 @@ -from cement import Controller, ex -from kontor_schema import KontorDB - - -class Database(Controller): - - class Meta: - label = 'database' - stacked_type = 'nested' - stacked_on = 'base' - - @ex( - help='export database to given file', - arguments=[ - (['-f', '--file'], - {'help': 'file to store database content', - 'action': 'store', - 'dest': 'db_file'}) - ], - ) - def export(self): - data = { - 'db_file': 'data.json', - 'export_type': 'JSON', - } - if self.app.pargs.db_file is not None: - data['db_file'] = self.app.pargs.db_file - kontor_db = KontorDB(self.app.engine, self.app.config, self.app.log) - kontor_db.export_db(data['export_type'], data['db_file']) - self.app.render(data, 'command1.jinja2') - - @ex( - label='import', - help='import data from file into database', - arguments=[ - (['-f', '--file'], - {'help': 'file to read data', - 'action': 'store', - 'dest': 'db_file'}) - ], - ) - def import_cmd(self): - data = { - 'db_file': 'data.json', - 'data_type': 'JSON', - } - if self.app.pargs.db_file is not None: - data['db_file'] = self.app.pargs.db_file - kontor_db = KontorDB(self.app.engine, self.app.config, self.app.log) - self.app.render(data, 'import.jinja2') - kontor_db.import_db(data['db_file'], self.app.pargs.dry_run) diff --git a/python/kontor-cli.backup/kontor/controllers/media.py b/python/kontor-cli.backup/kontor/controllers/media.py deleted file mode 100644 index ecee44d..0000000 --- a/python/kontor-cli.backup/kontor/controllers/media.py +++ /dev/null @@ -1,79 +0,0 @@ -from cement import Controller, ex - -from ..database import KontorDB - - -class Media(Controller): - - class Meta: - label = 'media' - stacked_type = 'nested' - stacked_on = 'base' - - @ex( - label='update', - help='update title for mediafiles', - ) - def update_title(self): - kontor_db = KontorDB(self.app.engine, self.app.config, self.app.log) - kontor_db.update_title(self.app.pargs.dry_run) - - @ex( - label='download', - help='download all marked videos', - arguments=[ - (['-d', '--dir'], - {'help': 'directory to store videos', - 'action': 'store', - 'dest': 'media_dir'}) - ], - ) - def download(self): - data = { - 'media_dir': '/data/media', - } - if self.app.pargs.media_dir is not None: - data['media_dir'] = self.app.pargs.media_dir - kontor_db = KontorDB(self.app.engine, self.app.config, self.app.log) - kontor_db.download_file(self.app.pargs.dry_run) - - @ex( - help='add url to database', - arguments=[ - (['-u', '--url'], - {'help': 'link to downloadable video', - 'action': 'store', - 'dest': 'link'}) - ], - ) - def add(self): - data = { - 'link_url': None - } - if self.app.pargs.link is not None: - data['link_url'] = self.app.pargs.link - if self.app.pargs.dry_run: - print(f"add url {data['link_url']} to database") - kontor_db = KontorDB(self.app.engine, self.app.config, self.app.log) - kontor_db.add_link(self.app.pargs.link, self.app.pargs.dry_run) - - else: - print("no url was given.") - - @ex( - help='check files if existing', - arguments=[ - (['-d', '--dir'], - {'help': 'directory to store videos', - 'action': 'store', - 'dest': 'media_dir'}) - ], - ) - def check(self): - data = { - 'media_dir': '/data/media', - } - if self.app.pargs.media_dir is not None: - data['media_dir'] = self.app.pargs.media_dir - kontor_db = KontorDB(self.app.engine, self.app.config, self.app.log) - kontor_db.check_files() diff --git a/python/kontor-cli.backup/kontor/core/__init__.py b/python/kontor-cli.backup/kontor/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/python/kontor-cli.backup/kontor/core/exc.py b/python/kontor-cli.backup/kontor/core/exc.py deleted file mode 100644 index 8dd63e8..0000000 --- a/python/kontor-cli.backup/kontor/core/exc.py +++ /dev/null @@ -1,4 +0,0 @@ - -class KontorCliError(Exception): - """Generic errors.""" - pass diff --git a/python/kontor-cli.backup/kontor/core/version.py b/python/kontor-cli.backup/kontor/core/version.py deleted file mode 100644 index 846814d..0000000 --- a/python/kontor-cli.backup/kontor/core/version.py +++ /dev/null @@ -1,7 +0,0 @@ - -from cement.utils.version import get_version as cement_get_version - -VERSION = (0, 1, 0, 'alpha', 0) - -def get_version(version=VERSION): - return cement_get_version(version) diff --git a/python/kontor-cli.backup/kontor/ext/__init__.py b/python/kontor-cli.backup/kontor/ext/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/python/kontor-cli.backup/kontor/main.py b/python/kontor-cli.backup/kontor/main.py deleted file mode 100644 index c8084b8..0000000 --- a/python/kontor-cli.backup/kontor/main.py +++ /dev/null @@ -1,124 +0,0 @@ - -from cement import App, TestApp, init_defaults -from cement.core.exc import CaughtSignal -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - -from .controllers.database import Database -from .controllers.media import Media -from .core.exc import KontorCliError -from .controllers.base import Base - -# configuration defaults -CONFIG = init_defaults('kontor', 'mariadb', 'media') -CONFIG['kontor']['foo'] = 'bar' -CONFIG['mariadb']['user'] = 'kontor' -CONFIG['mariadb']['password'] = 'kontor' -CONFIG['mariadb']['host'] = '127.0.0.1' -CONFIG['mariadb']['port'] = '3306' -CONFIG['mariadb']['database'] = 'kontor' -CONFIG['media']['yt-dlp'] = '/home/tpeetz/bin/yt-dlp' -CONFIG['media']['dir'] = '/data/media' -META = init_defaults('output.json', 'output.yaml') -META['output.json']['overridable'] = True -META['output.yaml']['overridable'] = True - - -def extend_sqlalchemy(app): - app.log.info('extending kontor application with sqlalchemy') - connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format( - app.config.get('mariadb', 'user'), - app.config.get('mariadb', 'password'), - app.config.get('mariadb', 'host'), - app.config.get('mariadb', 'port'), - app.config.get('mariadb', 'database') - )) - # engine = create_engine(connect_string, echo=True) - engine = create_engine(connect_string) - Base.metadata.create_all(bind=engine) - __session__ = sessionmaker(bind=engine) - app.extend('engine', engine) - - -class KontorCli(App): - """Kontor CLI primary application.""" - - class Meta: - label = 'kontor' - - # configuration defaults - config_defaults = CONFIG - - meta_defaults = META - - # call sys.exit() on close - exit_on_close = True - - # load additional framework extensions - extensions = [ - 'yaml', - 'json', - 'colorlog', - 'jinja2', - ] - - # configuration handler - config_handler = 'yaml' - - # configuration file suffix - config_file_suffix = '.yml' - - # set the log handler - log_handler = 'colorlog' - - # set the output handler - output_handler = 'jinja2' - - hooks = [ - ('post_setup', extend_sqlalchemy), - ] - - # register handlers - handlers = [ - Base, - Database, - Media, - ] - - -class KontorCliTest(TestApp,KontorCli): - """A sub-class of KontorCli that is better suited for testing.""" - - class Meta: - label = 'kontor' - - -def main(): - with KontorCli() as app: - try: - app.run() - - except AssertionError as e: - print('AssertionError > %s' % e.args[0]) - app.exit_code = 1 - - if app.debug is True: - import traceback - traceback.print_exc() - - except KontorCliError as e: - print('KontorCliError > %s' % e.args[0]) - app.exit_code = 1 - - if app.debug is True: - import traceback - traceback.print_exc() - - except CaughtSignal as e: - # Default Cement signals are SIGINT and SIGTERM, exit 0 (non-error) - print('\n%s' % e) - app.exit_code = 0 - - -if __name__ == '__main__': - main() diff --git a/python/kontor-cli.backup/kontor/plugins/__init__.py b/python/kontor-cli.backup/kontor/plugins/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/python/kontor-cli.backup/kontor/templates/__init__.py b/python/kontor-cli.backup/kontor/templates/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/python/kontor-cli.backup/kontor/templates/command1.jinja2 b/python/kontor-cli.backup/kontor/templates/command1.jinja2 deleted file mode 100644 index 2435e4d..0000000 --- a/python/kontor-cli.backup/kontor/templates/command1.jinja2 +++ /dev/null @@ -1,4 +0,0 @@ - -Example Template (templates/command1.jinja2) - -Foo => {{ foo }} diff --git a/python/kontor-cli.backup/kontor/templates/download.jinja2 b/python/kontor-cli.backup/kontor/templates/download.jinja2 deleted file mode 100644 index a6e3d50..0000000 --- a/python/kontor-cli.backup/kontor/templates/download.jinja2 +++ /dev/null @@ -1,2 +0,0 @@ - -Download videos to directory {{ media_dir }} diff --git a/python/kontor-cli.backup/kontor/templates/import.jinja2 b/python/kontor-cli.backup/kontor/templates/import.jinja2 deleted file mode 100644 index c54f162..0000000 --- a/python/kontor-cli.backup/kontor/templates/import.jinja2 +++ /dev/null @@ -1,2 +0,0 @@ - -Import data from {{ db_file }} diff --git a/python/kontor-cli.backup/kontor/templates/update.jinja2 b/python/kontor-cli.backup/kontor/templates/update.jinja2 deleted file mode 100644 index 992873f..0000000 --- a/python/kontor-cli.backup/kontor/templates/update.jinja2 +++ /dev/null @@ -1,2 +0,0 @@ - -Update entries of mediafile diff --git a/python/kontor-cli.backup/requirements-dev.txt b/python/kontor-cli.backup/requirements-dev.txt deleted file mode 100644 index f20606e..0000000 --- a/python/kontor-cli.backup/requirements-dev.txt +++ /dev/null @@ -1,8 +0,0 @@ --r requirements.txt - -pytest -pytest-cov -coverage -twine>=1.11.0 -setuptools>=38.6.0 -wheel>=0.31.0 diff --git a/python/kontor-cli.backup/requirements.txt b/python/kontor-cli.backup/requirements.txt deleted file mode 100644 index bee2df1..0000000 --- a/python/kontor-cli.backup/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -cement==3.0.12 -cement[jinja2] -cement[yaml] -cement[colorlog] -mariadb -sqlalchemy diff --git a/python/kontor-cli.backup/setup.cfg b/python/kontor-cli.backup/setup.cfg deleted file mode 100644 index e69de29..0000000 diff --git a/python/kontor-cli.backup/setup.py b/python/kontor-cli.backup/setup.py deleted file mode 100644 index 185d9fa..0000000 --- a/python/kontor-cli.backup/setup.py +++ /dev/null @@ -1,28 +0,0 @@ - -from setuptools import setup, find_packages -from kontor.core.version import get_version - -VERSION = get_version() - -f = open('README.md', 'r') -LONG_DESCRIPTION = f.read() -f.close() - -setup( - name='kontor', - version=VERSION, - description='Kontor CLI', - long_description=LONG_DESCRIPTION, - long_description_content_type='text/markdown', - author='Thomas Peetz', - author_email='thomas.peetz@thpeetz.de', - url='https://gitlab.com/tpeetz/kontor/', - license='MIT', - packages=find_packages(exclude=['ez_setup', 'tests*']), - package_data={'kontor': ['templates/*']}, - include_package_data=True, - entry_points=""" - [console_scripts] - kontor = kontor.main:main - """, -) diff --git a/python/kontor-cli.backup/tests/conftest.py b/python/kontor-cli.backup/tests/conftest.py deleted file mode 100644 index 5124e2e..0000000 --- a/python/kontor-cli.backup/tests/conftest.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -PyTest Fixtures. -""" - -import pytest -from cement import fs - -@pytest.fixture(scope="function") -def tmp(request): - """ - Create a `tmp` object that geneates a unique temporary directory, and file - for each test function that requires it. - """ - t = fs.Tmp() - yield t - t.remove() diff --git a/python/kontor-cli.backup/tests/test_kontor.py b/python/kontor-cli.backup/tests/test_kontor.py deleted file mode 100644 index e93dd44..0000000 --- a/python/kontor-cli.backup/tests/test_kontor.py +++ /dev/null @@ -1,36 +0,0 @@ - -from pytest import raises -from kontor.main import KontorCliTest - -def test_kontor(): - # test kontor without any subcommands or arguments - with KontorCliTest() as app: - app.run() - assert app.exit_code == 0 - - -def test_kontor_debug(): - # test that debug mode is functional - argv = ['--debug'] - with KontorCliTest(argv=argv) as app: - app.run() - assert app.debug is True - - -def test_command1(): - # test command1 without arguments - argv = ['command1'] - with KontorCliTest(argv=argv) as app: - app.run() - data,output = app.last_rendered - assert data['foo'] == 'bar' - assert output.find('Foo => bar') - - - # test command1 with arguments - argv = ['command1', '--foo', 'not-bar'] - with KontorCliTest(argv=argv) as app: - app.run() - data,output = app.last_rendered - assert data['foo'] == 'not-bar' - assert output.find('Foo => not-bar') diff --git a/python/kontor-cli/kontor/controllers/base.py b/python/kontor-cli/kontor/controllers/base.py index 0f7bdda..931db26 100644 --- a/python/kontor-cli/kontor/controllers/base.py +++ b/python/kontor-cli/kontor/controllers/base.py @@ -32,29 +32,3 @@ class CliBase(Controller): """Default action if no sub-command is passed.""" self.app.args.print_help() - - - @ex( - help='example sub command1', - - # sub-command level arguments. ex: 'kontor command1 --foo bar' - arguments=[ - ### add a sample foo option under subcommand namespace - ( [ '-f', '--foo' ], - { 'help' : 'notorious foo option', - 'action' : 'store', - 'dest' : 'foo' } ), - ], - ) - def command1(self): - """Example sub-command.""" - - data = { - 'foo' : 'bar', - } - - ### do something with arguments - if self.app.pargs.foo is not None: - data['foo'] = self.app.pargs.foo - - self.app.render(data, 'command1.jinja2') diff --git a/python/kontor-cli/kontor/controllers/database.py b/python/kontor-cli/kontor/controllers/database.py index a289c99..7f2fada 100644 --- a/python/kontor-cli/kontor/controllers/database.py +++ b/python/kontor-cli/kontor/controllers/database.py @@ -8,6 +8,11 @@ class Database(Controller): label = 'database' stacked_type = 'nested' stacked_on = 'clibase' + arguments = [ + (['-m', '--dry-run'], + {'action': 'store_true', + 'dest': 'dry_run'}), + ] @ex( help='export database to given file', @@ -25,9 +30,11 @@ class Database(Controller): } if self.app.pargs.db_file is not None: data['db_file'] = self.app.pargs.db_file - kontor_db = KontorDB(self.app.engine, self.app.config, self.app.log) - kontor_db.export_db(data['export_type'], data['db_file']) - self.app.render(data, 'command1.jinja2') + kontor_db = KontorDB(self.app.engine) + self.app.log.info(f"export DB to {data['db_file']} as {data['export_type']}") + results = kontor_db.export_db(data['export_type'], data['db_file']) + data['results'] = results + self.app.render(data, 'export_db.jinja2') @ex( label='import', @@ -46,8 +53,7 @@ class Database(Controller): } if self.app.pargs.db_file is not None: data['db_file'] = self.app.pargs.db_file - kontor_db = KontorDB(self.app.engine, self.app.config, self.app.log) - self.app.render(data, 'import.jinja2') + kontor_db = KontorDB(self.app.engine) kontor_db.import_db(data['db_file'], self.app.pargs.dry_run) @ex( @@ -66,7 +72,7 @@ class Database(Controller): cursor.execute("SHOW TABLES") for (tablename,) in cursor.fetchall(): table_list.append(tablename) - kontor_db = KontorDB(self.app.engine, self.app.log) + kontor_db = KontorDB(self.app.engine) table_names = kontor_db.get_table_names() for table in table_list: if table not in table_names: diff --git a/python/kontor-cli/kontor/controllers/media.py b/python/kontor-cli/kontor/controllers/media.py index 7979b25..b1fd262 100644 --- a/python/kontor-cli/kontor/controllers/media.py +++ b/python/kontor-cli/kontor/controllers/media.py @@ -1,9 +1,11 @@ +from pathlib import Path + from cement import Controller, ex from kontor_schema import KontorDB from kontor_video import VideoLink -class Media(Controller): +class Media(Controller): class Meta: label = 'media' stacked_type = 'nested' @@ -14,8 +16,14 @@ class Media(Controller): help='update title for mediafiles', ) def update_title(self): - kontor_db = KontorDB(self.app.engine, self.app.log) - kontor_db.update_title(self.app.pargs.dry_run) + kontor_db = KontorDB(self.app.engine) + updates = kontor_db.get_update_list() + self.app.log.info(f"found {len(updates)} links for update") + for file_id, url in updates.items(): + link = VideoLink(url) + title = link.get_title() + if title is not None: + kontor_db.update_entry('media_file', file_id, {'title': title, 'review': 0,}) @ex( label='download', @@ -33,11 +41,26 @@ class Media(Controller): } if self.app.pargs.media_dir is not None: data['media_dir'] = self.app.pargs.media_dir - kontor_db = KontorDB(self.app.engine, self.app.log) + kontor_db = KontorDB(self.app.engine) downloads = kontor_db.get_download_list() - for download in downloads: - link = VideoLink(download, download_dir=data['media_dir']) - link.download() + self.app.log.info(f"found {len(downloads)} links for download") + for file_id, url in downloads.items(): + link = VideoLink(url) + file_name = link.download(download_dir=data['media_dir']) + if file_name is None: + kontor_db.update_entry('media_file', file_id, {'file_name': None, 'should_download': 1}) + else: + download_file = Path(file_name) + download_file.with_name(f"{file_id}{download_file.suffix}") + link.file_name = download_file.name + link.should_download = 0 + link.cloud_link = download_file.absolute() + kontor_db.update_entry('media_file', file_id, + { + 'file_name': download_file.name, + 'should_download': 0, + 'cloud_link': download_file.absolute()} + ) @ex( help='add url to database', @@ -56,9 +79,10 @@ class Media(Controller): data['link_url'] = self.app.pargs.link if self.app.pargs.dry_run: print(f"add url {data['link_url']} to database") - kontor_db = KontorDB(self.app.engine, self.app.log) - kontor_db.add_link(self.app.pargs.link, self.app.pargs.dry_run) - + else: + kontor_db = KontorDB(self.app.engine) + result = kontor_db.add_link(self.app.pargs.link) + self.log.info(result) else: print("no url was given.") @@ -77,5 +101,5 @@ class Media(Controller): } if self.app.pargs.media_dir is not None: data['media_dir'] = self.app.pargs.media_dir - kontor_db = KontorDB(self.app.engine, self.app.log) + kontor_db = KontorDB(self.app.engine) kontor_db.check_files() diff --git a/python/kontor-cli/kontor/templates/command1.jinja2 b/python/kontor-cli/kontor/templates/command1.jinja2 deleted file mode 100644 index 2435e4d..0000000 --- a/python/kontor-cli/kontor/templates/command1.jinja2 +++ /dev/null @@ -1,4 +0,0 @@ - -Example Template (templates/command1.jinja2) - -Foo => {{ foo }} diff --git a/python/kontor-cli/kontor/templates/export_db.jinja2 b/python/kontor-cli/kontor/templates/export_db.jinja2 new file mode 100644 index 0000000..678aaa3 --- /dev/null +++ b/python/kontor-cli/kontor/templates/export_db.jinja2 @@ -0,0 +1,5 @@ + Following tables were exported: + +{% for key, value in results.items() %} +Table {{key}}: {{value}} entries +{% endfor %} diff --git a/python/kontor-cli/pyproject.toml b/python/kontor-cli/pyproject.toml new file mode 100644 index 0000000..f65e841 --- /dev/null +++ b/python/kontor-cli/pyproject.toml @@ -0,0 +1,29 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "kontor-cli" +version = "0.1.0" +description = "Kontor CLI Application" +authors = [ + {name = "Thomas Peetz", email = "thomas.peetz@thpeetz.de"}, +] +maintainers = [ + {name = "Thomas Peetz", email = "thomas.peetz@thpeetz.de"}, +] +readme = "README.md" +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Programming Language :: Python", + "Programming Language :: Python :: 3.10", + "Topic :: Utilities", +] + +dependencies = [ +] +requires-python = ">= 3.10" + +[projects.scripts] +kontor = "kontor.main:main" diff --git a/python/kontor-cli/requirements.txt b/python/kontor-cli/requirements.txt index 65cbadd..58833ba 100644 --- a/python/kontor-cli/requirements.txt +++ b/python/kontor-cli/requirements.txt @@ -7,3 +7,4 @@ cement[yaml] cement[colorlog] mariadb sqlalchemy +pathlib diff --git a/python/kontor-schema/kontor_schema/__init__.py b/python/kontor-schema/kontor_schema/__init__.py index a70aa6d..a26e041 100644 --- a/python/kontor-schema/kontor_schema/__init__.py +++ b/python/kontor-schema/kontor_schema/__init__.py @@ -5,7 +5,6 @@ import uuid from datetime import datetime from pathlib import Path -import requests from sqlalchemy import Engine from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import sessionmaker @@ -20,9 +19,8 @@ from .media import MediaFile, MediaArticle, MediaVideo class KontorDB: - def __init__(self, db_engine: Engine, log): + def __init__(self, db_engine: Engine): self.engine = db_engine - self.log = log self.registry = {} self.init_registry() @@ -107,7 +105,7 @@ class KontorDB: columns[column.column_name] = {"order": column.column_order, "type": column.column_type} return columns - def get_filters(self, table_name): + def get_filters(self, table_name: str) -> dict: _filter_map = {} __session__ = sessionmaker(self.engine) with __session__() as session: @@ -116,7 +114,6 @@ class KontorDB: filter(MetaDataTable.table_name == table_name). filter(MetaDataColumn.show_filter == 1).all()): _filter_map[column.column_name] = {'label': column.filter_label, 'widget': None} - self.log.debug(f"retrieved {len(_filter_map)} filters: {_filter_map}") return _filter_map def data(self, table_name: str, columns: dict, filters: dict) -> list: @@ -135,19 +132,16 @@ class KontorDB: column_name = columns[order]['column'] if str(column_name).endswith("_id"): ref_table = column_name[:-3] - # print(f"{ref_table=}") ref = getattr(entry, ref_table) value = getattr(ref, "name") - # print(f"{value=}") row.append(value) else: row.append(getattr(entry, column_name)) - # print(repr(row)) data.append(row) return data - def export_db(self, export_type: str, export_file_name: str): - self.log.info(f"export DB to {export_file_name} as {export_type}") + def export_db(self, export_type: str, export_file_name: str) -> dict: + results = {} db = {} export_table_list = self.get_table_names() for table in export_table_list: @@ -161,8 +155,6 @@ class KontorDB: with __session__() as session: rows = session.query(model).all() entries = [] - self.log.debug(f"found {len(rows)} entries") - self.log.debug(f"found {len(columns)} columns") for row in rows: # print(row) entry = {} @@ -176,11 +168,11 @@ class KontorDB: entry[column_name] = str(value) else: entry[column_name] = value - except AttributeError as error: - self.log.debug("could not get value") + except AttributeError: + pass entries.append(entry) db[table] = entries - export_file = Path(export_file_name) + results[table] = len(entries) match export_type: case "JSON": json_dump = json.dumps(db, indent=4) @@ -190,17 +182,14 @@ class KontorDB: export_file = Path(export_file_name) case "SQLite": export_file = Path(export_file_name) - case _: - self.log.debug("unknown export type") - if export_file.exists(): - self.log.debug(f"{export_file} exists") + return results - def import_db(self, import_file_name: str, dry_run: bool): + def import_db(self, import_file_name: str, dry_run: bool) -> dict: + result = {} import_file = Path(import_file_name) if not import_file.exists(): print(f"File {import_file_name} does not exist. Do nothing.") - return - self.log.debug(f"evaluate type from file extension: {import_file.suffix}") + return result match import_file.suffix: case '.json': print("read json file") @@ -208,34 +197,44 @@ class KontorDB: json_load = json.load(json_file) for table in json_load: print(f"{table}: {len(json_load[table])}") - self.import_table(table, json_load[table], dry_run) + result[table] = self.import_table(table, json_load[table], dry_run) case '.yml': print("read yaml file") case '.yaml': print("read yaml file") case '.db': print("read sqlite file") + return result - def import_table(self, table_name, items, dry_run: bool): + def import_table(self, table_name, items, dry_run: bool) -> dict: + result = {} + updated = [] + added = [] + remaining = [] existing_ids = self.get_ids(table_name) for item in items: - # self.log.debug(f"{item}") current_id = item['id'] found_item = None __session__ = sessionmaker(self.engine) with __session__() as session: found_item = session.query(self.registry[table_name]).get(current_id) - self.log.debug(f"found: {found_item}") - if found_item is not None: - changed = self.update_entry(found_item, item, dry_run) - if changed: - print(f"{current_id} has changed") - existing_ids.remove(current_id) - else: - self.log.info("item to import not found in database, add new one...") - self.add_entry(table_name, item, session, dry_run) + if found_item is not None: + changed = self.update_entry(table_name, current_id, item) + updated.append(item) + if changed: + print(f"{current_id} has changed") + updated.append(item) + existing_ids.remove(current_id) + else: + self.add_entry(table_name, item) + added.append(item) if len(existing_ids) > 0: print("remaining items") + remaining.extend(existing_ids) + result['updated'] = updated + result['added'] = added + result['remaining'] = remaining + return result def get_ids(self, table_name: str) -> list: existing_ids = [] @@ -246,36 +245,36 @@ class KontorDB: existing_ids.append(getattr(item, 'id')) return existing_ids - def add_entry(self, table_name: str, update_item: dict, session, dry_run: bool): - add_item = self.registry[table_name]() - for key in update_item.keys(): - update_value = update_item[key] - setattr(add_item, key, update_value) - if dry_run: - self.log.info(f"add item {type(add_item)} with id {update_item['id']}") - else: - session.add(add_item) - session.commit() + def add_entry(self, table_name: str, update_item: dict): + __session__ = sessionmaker(self.engine) + with __session__() as session: + add_item = self.registry[table_name]() + for key in update_item.keys(): + update_value = update_item[key] + setattr(add_item, key, update_value) + session.add(add_item) + session.commit() - def update_entry(self, existing_item, update_item: dict, dry_run: bool) -> bool: - changed = False - for key in update_item.keys(): - update_value = update_item[key] - existing_value = getattr(existing_item, key) - if type(existing_value) is not type(update_value): - # self.log.debug(f"compare {type(existing_value)} with {type(update_value)}") - existing_value = str(existing_value) - if existing_value != update_value: - print(f"{key} has changed: {existing_value} != {update_value}") - if not dry_run: + def update_entry(self, table_name, current_id, update_item: dict) -> bool: + __session__ = sessionmaker(self.engine) + with __session__() as session: + existing_item = session.query(self.registry[table_name]).get(current_id) + changed = False + for key in update_item.keys(): + update_value = update_item[key] + existing_value = getattr(existing_item, key) + if type(existing_value) is not type(update_value): + existing_value = str(existing_value) + if existing_value != update_value: + print(f"{key} has changed: {existing_value} != {update_value}") setattr(existing_item, key, update_value) - # existing_item[key] = update_value + session.commit() changed = True - self.log.info(f"update {key} with {update_value}") + print(f"update {key} with {update_value}") return changed - def add_link(self, link: str, dry_run: bool): - self.log.info(f"add link {link} to media_file") + def add_link(self, link: str) -> dict: + result = {} __session__ = sessionmaker(self.engine) with __session__() as session: media_file = MediaFile() @@ -289,125 +288,32 @@ class KontorDB: try: session.add(media_file) session.commit() - self.log.info(f"entry {media_file} successfully added") + result['added'] = media_file except IntegrityError as error: session.rollback() - self.log.info(error.orig) + result['error'] = error.orig + return result - def update_title(self, dry_run=False): - self.log.info(f"get links to review of media_file") - __session__ = sessionmaker(self.engine) - with __session__() as session: - links = session.query(MediaFile).filter(MediaFile.review == 1).all() - self.log.info(f"try to update {len(links)} items") - for link in links: - url = link.url - if url is None: - self.log.info(f"url has not been set for {link.id}") - continue - self.log.info('get title for url {}'.format(url)) - if dry_run: - continue - try: - r = requests.get(url) - soup = BeautifulSoup(r.content, "html.parser") - title = soup.title.string - except: - self.log.info("Sorry, could not retrieve title") - continue - self.log.info('ID {} has title {}'.format(link.id, title)) - link.title = title - link.review = 0 - session.commit() - - def get_update_list(self) -> list[str]: - self.log.debug("get links marked as review") - update_list = [] + def get_update_list(self) -> dict: + update_list = {} __session__ = sessionmaker(self.engine) with __session__() as session: links = session.query(MediaFile).filter(MediaFile.review == 1).all() for link in links: url = link.url if url is None: - self.log.info(f"url has not been set for {link.id}") continue - update_list.append(url) - self.log.debug(f"found {len(update_list)} urls for updates") + update_list[link.id] = url return update_list - def get_download_list(self) -> list[str]: - self.log.debug("get links marked as should_download") - download_list = [] + def get_download_list(self) -> dict: + download_list = {} __session__ = sessionmaker(self.engine) with __session__() as session: links = session.query(MediaFile).filter(MediaFile.should_download == 1).all() for link in links: url = link.url if url is None: - self.log.info(f"url has not been set for {link.id}") continue - download_list.append(url) - self.log.debug(f"found {len(download_list)} urls for downloads") + download_list[link.id] = url return download_list - - def download_file(self, dry_run=False): - self.log.info(f"download marked urls of media_file") - __session__ = sessionmaker(self.engine) - with __session__() as session: - links = session.query(MediaFile).filter(MediaFile.should_download == 1).all() - self.log.info(f"try to download {len(links)} items") - for link in links: - url = link.url - if url is None: - self.log.info(f"url has not been set for {link.id}") - continue - if dry_run: - self.log.info(f"download {link.url} to {self.config.get('media', 'dir')}") - continue - filename = self.download_url(link) - if filename is None: - link.file_name = filename - link.should_download = 1 - else: - download_file = Path(filename) - download_file.with_name(f"{link.id}{download_file.suffix}") - link.file_name = download_file.name - link.should_download = 0 - link.cloud_link = download_file.absolute() - session.commit() - - def parse_output(self, lines_list): - file_name = "" - for line in lines_list: - if 'has already been downloaded' in line: - end_len = len(' has already been downloaded') - file_name = line[11:-end_len] - self.log.info('found file: "%s"', file_name) - if 'Destination' in line: - line_len = len(line) - start_len = len('[download] Destination: ') - file_len = line_len - start_len - file_name = line[-file_len:] - self.log.info('new file: "%s"', file_name) - return file_name - - def download_url(self, video_url): - media_dir = Path(self.config.get('media', 'dir')) - if not media_dir.exists(): - media_dir = Path().absolute() - self.log.info(f"download video to {media_dir}") - result = subprocess.run([self.config.get('media', 'yt-dlp'), video_url], cwd=media_dir, capture_output=True, - text=True) - if result.returncode == 0: - output = result.stdout - output = re.sub(' +', ' ', output) - lines_list = output.splitlines() - return self.parse_output(lines_list) - else: - return None - - def check_files(self): - media_dir = Path(self.config.get('media', 'dir')) - if not media_dir.exists(): - return - self.log.info(f"check files in {media_dir}") diff --git a/python/kontor-video/kontor_video/__init__.py b/python/kontor-video/kontor_video/__init__.py index 6fd7843..8cfb548 100644 --- a/python/kontor-video/kontor_video/__init__.py +++ b/python/kontor-video/kontor_video/__init__.py @@ -1,21 +1,63 @@ +import re +import subprocess +from pathlib import Path + import requests from bs4 import BeautifulSoup class VideoLink: - def __init__(self, url: str, log): + def __init__(self, url: str, dl_tool: str, table: str): + self.file_name = None self.url = url self.title = None - self.log = log + self.dl_tool = dl_tool + self.table = table - def get_title(self): + def get_title(self) -> str: try: r = requests.get(self.url) soup = BeautifulSoup(r.content, "html.parser") title = soup.title.string except: - self.log.info("Sorry, could not retrieve title") + title = None + return title + def download(self, download_dir=None): - self.log.info(f"download {self.url} to {download_dir}") + if download_dir is None: + download_dir = Path.cwd() + result = subprocess.run([self.dl_tool, self.url], cwd=download_dir, capture_output=True, text=True) + if result.returncode == 0: + output = result.stdout + output = re.sub(' +', ' ', output) + lines_list = output.splitlines() + return self.__parse_output__(lines_list) + else: + return None + + def __parse_output__(self, lines_list): + self.file_name = "" + for line in lines_list: + if 'has already been downloaded' in line: + end_len = len(' has already been downloaded') + self.file_name = line[11:-end_len] + if 'Destination' in line: + line_len = len(line) + start_len = len('[download] Destination: ') + file_len = line_len - start_len + self.file_name = line[-file_len:] + return self.file_name + + +class MediaFile(VideoLink): + + def __init__(self, url: str, dl_tool='yt-dlp'): + super().__init__(url, dl_tool, 'media_file') + + +class MediaVideo(VideoLink): + + def __init__(self, url: str, dl_tool='yt-dlp'): + super().__init__(url, dl_tool, 'media_video') From 4e884fdbe52146167f37d3ea0b39cbf6bf132a5a Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Tue, 21 Jan 2025 16:46:12 +0100 Subject: [PATCH 60/91] fix exporting and importing from file --- .../kontor-cli/kontor/controllers/database.py | 24 +- python/kontor-cli/kontor/controllers/media.py | 22 +- python/kontor-cli/kontor/main.py | 4 +- python/kontor-gui/.gitignore | 1 + python/kontor-gui/data.json | 34939 ++++++++++++++++ python/kontor-gui/gui/comic_window.py | 54 + python/kontor-gui/gui/main_window.py | 14 +- python/kontor-gui/kontor.py | 3 +- .../kontor-schema/kontor_schema/__init__.py | 125 +- .../kontor/admin/SetupModuleAdmin.java | 4 +- 10 files changed, 35114 insertions(+), 76 deletions(-) create mode 100644 python/kontor-gui/data.json create mode 100644 python/kontor-gui/gui/comic_window.py diff --git a/python/kontor-cli/kontor/controllers/database.py b/python/kontor-cli/kontor/controllers/database.py index 7f2fada..c748f75 100644 --- a/python/kontor-cli/kontor/controllers/database.py +++ b/python/kontor-cli/kontor/controllers/database.py @@ -1,6 +1,5 @@ import mariadb from cement import Controller, ex -from kontor_schema import KontorDB class Database(Controller): @@ -30,9 +29,9 @@ class Database(Controller): } if self.app.pargs.db_file is not None: data['db_file'] = self.app.pargs.db_file - kontor_db = KontorDB(self.app.engine) + db = self.app.kontor_db self.app.log.info(f"export DB to {data['db_file']} as {data['export_type']}") - results = kontor_db.export_db(data['export_type'], data['db_file']) + results = db.export_db(data['export_type'], data['db_file']) data['results'] = results self.app.render(data, 'export_db.jinja2') @@ -43,18 +42,25 @@ class Database(Controller): (['-f', '--file'], {'help': 'file to read data', 'action': 'store', - 'dest': 'db_file'}) + 'dest': 'db_file'}), + (['-d', '--delete-first'], + {'help': 'delete existing entries before import', + 'action': 'store_true', + 'dest': 'delete_first'}) ], ) def import_cmd(self): data = { 'db_file': 'data.json', 'data_type': 'JSON', + 'delete_first': False, } if self.app.pargs.db_file is not None: data['db_file'] = self.app.pargs.db_file - kontor_db = KontorDB(self.app.engine) - kontor_db.import_db(data['db_file'], self.app.pargs.dry_run) + if self.app.pargs.delete_first is not None: + data['delete_first'] = self.app.pargs.delete_first + db = self.app.kontor_db + db.import_db(data['db_file'], data['delete_first']) @ex( help='check the db schema against MetaDataTable and MetaDataColumn' @@ -72,13 +78,13 @@ class Database(Controller): cursor.execute("SHOW TABLES") for (tablename,) in cursor.fetchall(): table_list.append(tablename) - kontor_db = KontorDB(self.app.engine) - table_names = kontor_db.get_table_names() + db = self.app.kontor_db + table_names = db.get_table_names() for table in table_list: if table not in table_names: self.app.log.info(f"{table} is not stored in MetaDataTable") continue - meta_data = kontor_db.get_columns(table) + meta_data = db.get_columns(table) field_info = self.get_table_field_info(cursor, table) for column in field_info: if column not in meta_data: diff --git a/python/kontor-cli/kontor/controllers/media.py b/python/kontor-cli/kontor/controllers/media.py index b1fd262..b5ec320 100644 --- a/python/kontor-cli/kontor/controllers/media.py +++ b/python/kontor-cli/kontor/controllers/media.py @@ -16,14 +16,14 @@ class Media(Controller): help='update title for mediafiles', ) def update_title(self): - kontor_db = KontorDB(self.app.engine) - updates = kontor_db.get_update_list() + db = self.app.kontor_db + updates = db.get_update_list() self.app.log.info(f"found {len(updates)} links for update") for file_id, url in updates.items(): link = VideoLink(url) title = link.get_title() if title is not None: - kontor_db.update_entry('media_file', file_id, {'title': title, 'review': 0,}) + db.update_entry('media_file', file_id, {'title': title, 'review': 0,}) @ex( label='download', @@ -41,21 +41,21 @@ class Media(Controller): } if self.app.pargs.media_dir is not None: data['media_dir'] = self.app.pargs.media_dir - kontor_db = KontorDB(self.app.engine) - downloads = kontor_db.get_download_list() + db = self.app.kontor_db + downloads = db.get_download_list() self.app.log.info(f"found {len(downloads)} links for download") for file_id, url in downloads.items(): link = VideoLink(url) file_name = link.download(download_dir=data['media_dir']) if file_name is None: - kontor_db.update_entry('media_file', file_id, {'file_name': None, 'should_download': 1}) + db.update_entry('media_file', file_id, {'file_name': None, 'should_download': 1}) else: download_file = Path(file_name) download_file.with_name(f"{file_id}{download_file.suffix}") link.file_name = download_file.name link.should_download = 0 link.cloud_link = download_file.absolute() - kontor_db.update_entry('media_file', file_id, + db.update_entry('media_file', file_id, { 'file_name': download_file.name, 'should_download': 0, @@ -80,8 +80,8 @@ class Media(Controller): if self.app.pargs.dry_run: print(f"add url {data['link_url']} to database") else: - kontor_db = KontorDB(self.app.engine) - result = kontor_db.add_link(self.app.pargs.link) + db = self.app.kontor_db + result = db.add_link(self.app.pargs.link) self.log.info(result) else: print("no url was given.") @@ -101,5 +101,5 @@ class Media(Controller): } if self.app.pargs.media_dir is not None: data['media_dir'] = self.app.pargs.media_dir - kontor_db = KontorDB(self.app.engine) - kontor_db.check_files() + db = self.app.kontor_db + db.check_files() diff --git a/python/kontor-cli/kontor/main.py b/python/kontor-cli/kontor/main.py index 2d71a3e..4eb51e8 100644 --- a/python/kontor-cli/kontor/main.py +++ b/python/kontor-cli/kontor/main.py @@ -1,7 +1,7 @@ from cement import App, TestApp, init_defaults from cement.core.exc import CaughtSignal -from kontor_schema.base import Base +from kontor_schema import Base, KontorDB from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker @@ -35,6 +35,8 @@ def extend_sqlalchemy(app): Base.metadata.create_all(bind=engine, checkfirst=True) __session__ = sessionmaker(bind=engine) app.extend('engine', engine) + kontor_db = KontorDB(engine, app.log) + app.extend('kontor_db', kontor_db) class Kontor(App): diff --git a/python/kontor-gui/.gitignore b/python/kontor-gui/.gitignore index 9c0554f..e877d19 100644 --- a/python/kontor-gui/.gitignore +++ b/python/kontor-gui/.gitignore @@ -6,3 +6,4 @@ include/ lib/ lib64/ lib64 +env/ diff --git a/python/kontor-gui/data.json b/python/kontor-gui/data.json new file mode 100644 index 0000000..5a8ef1b --- /dev/null +++ b/python/kontor-gui/data.json @@ -0,0 +1,34939 @@ +{ + "meta_data_table": [ + { + "id": "058fa05e-2dab-4f61-8415-b5beec1fe79e", + "created_date": "2025-01-20 16:04:41.598000", + "last_modified_date": "2025-01-20 16:04:41.598000", + "version": 0, + "table_name": "meta_data_table" + }, + { + "id": "13ea1a84-f92e-4ce6-b709-fe7c1893d229", + "created_date": "2025-01-20 16:04:41.160000", + "last_modified_date": "2025-01-20 16:04:41.160000", + "version": 0, + "table_name": "volume" + }, + { + "id": "1850a718-dd07-4766-9856-591d311a4d0d", + "created_date": "2025-01-20 16:04:41.362000", + "last_modified_date": "2025-01-20 16:04:41.362000", + "version": 0, + "table_name": "sport" + }, + { + "id": "1951f2b6-3640-4568-9c47-a20e0768a843", + "created_date": "2025-01-20 16:04:41.256000", + "last_modified_date": "2025-01-20 16:04:41.256000", + "version": 0, + "table_name": "author" + }, + { + "id": "245402a4-ee6a-404b-afb5-6a95ed88469f", + "created_date": "2025-01-20 16:04:41.607000", + "last_modified_date": "2025-01-20 16:04:41.607000", + "version": 0, + "table_name": "meta_data_column" + }, + { + "id": "26478de9-0bcc-427f-ba60-0a1ab79e107b", + "created_date": "2025-01-20 16:04:41.106000", + "last_modified_date": "2025-01-20 16:04:41.106000", + "version": 0, + "table_name": "comic" + }, + { + "id": "2f841ba9-48f3-4a44-876a-67044d43b5db", + "created_date": "2025-01-20 16:04:41.009000", + "last_modified_date": "2025-01-20 16:04:41.009000", + "version": 0, + "table_name": "media_video" + }, + { + "id": "33b748ee-9f55-487a-a614-2ab64b844a13", + "created_date": "2025-01-20 16:04:41.091000", + "last_modified_date": "2025-01-20 16:04:41.091000", + "version": 0, + "table_name": "publisher" + }, + { + "id": "36c7ac9c-270d-40a4-af43-c952120f165c", + "created_date": "2025-01-20 16:04:41.536000", + "last_modified_date": "2025-01-20 16:04:41.536000", + "version": 0, + "table_name": "authorization_matrix" + }, + { + "id": "3773ac76-a957-42b2-b24d-5e66e4c32c12", + "created_date": "2025-01-20 16:04:41.409000", + "last_modified_date": "2025-01-20 16:04:41.409000", + "version": 0, + "table_name": "vendor" + }, + { + "id": "3ce78ef2-35c6-4397-8cdf-9241b52236df", + "created_date": "2025-01-20 16:04:41.438000", + "last_modified_date": "2025-01-20 16:04:41.438000", + "version": 0, + "table_name": "rooster" + }, + { + "id": "63285ce5-eb45-47f5-9575-dec1f9778bbc", + "created_date": "2025-01-20 16:04:40.957000", + "last_modified_date": "2025-01-20 16:04:40.957000", + "version": 0, + "table_name": "media_article" + }, + { + "id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c", + "created_date": "2025-01-20 16:04:41.483000", + "last_modified_date": "2025-01-20 16:04:41.483000", + "version": 0, + "table_name": "user" + }, + { + "id": "721a49ce-a02d-4035-add9-126a4591cbb8", + "created_date": "2025-01-20 16:04:41.271000", + "last_modified_date": "2025-01-20 16:04:41.271000", + "version": 0, + "table_name": "article" + }, + { + "id": "727a04ab-67dd-4a80-b80c-d824e41bcda2", + "created_date": "2025-01-20 16:04:41.468000", + "last_modified_date": "2025-01-20 16:04:41.468000", + "version": 0, + "table_name": "card" + }, + { + "id": "777015a8-f8b8-43b2-b285-8aae3885b7e7", + "created_date": "2025-01-20 16:04:41.047000", + "last_modified_date": "2025-01-20 16:04:41.047000", + "version": 0, + "table_name": "media_file" + }, + { + "id": "7ebdc79a-d0a7-42dc-9e50-d93c79e182af", + "created_date": "2025-01-20 16:04:41.288000", + "last_modified_date": "2025-01-20 16:04:41.288000", + "version": 0, + "table_name": "article_author" + }, + { + "id": "815b78c9-aef7-42b8-9f16-06a93a3761b3", + "created_date": "2025-01-20 16:04:41.304000", + "last_modified_date": "2025-01-20 16:04:41.304000", + "version": 0, + "table_name": "book" + }, + { + "id": "925989d3-faa9-4bb4-9183-3564b29063ec", + "created_date": "2025-01-20 16:04:41.523000", + "last_modified_date": "2025-01-20 16:04:41.523000", + "version": 0, + "table_name": "role" + }, + { + "id": "9a1a6de3-eae1-48e7-88c4-0c67908ec1bf", + "created_date": "2025-01-20 16:04:41.421000", + "last_modified_date": "2025-01-20 16:04:41.421000", + "version": 0, + "table_name": "field_position" + }, + { + "id": "a7a24f23-586b-4ac4-97ab-b610e2ab2e5d", + "created_date": "2025-01-20 16:04:41.454000", + "last_modified_date": "2025-01-20 16:04:41.454000", + "version": 0, + "table_name": "card_set" + }, + { + "id": "a9170107-b28f-4476-947e-0aaa3a2a1e01", + "created_date": "2025-01-20 16:04:41.393000", + "last_modified_date": "2025-01-20 16:04:41.393000", + "version": 0, + "table_name": "team" + }, + { + "id": "ae68f37b-9314-472c-86ea-387486f4db1c", + "created_date": "2025-01-20 16:04:41.235000", + "last_modified_date": "2025-01-20 16:04:41.235000", + "version": 0, + "table_name": "comic_work" + }, + { + "id": "b7107df0-c08d-4370-9eff-8d7ebdea14ef", + "created_date": "2025-01-20 16:04:41.216000", + "last_modified_date": "2025-01-20 16:04:41.216000", + "version": 0, + "table_name": "worktype" + }, + { + "id": "c9060971-3f7d-4eb1-89e9-bb40d8687e77", + "created_date": "2025-01-20 16:04:41.076000", + "last_modified_date": "2025-01-20 16:04:41.076000", + "version": 0, + "table_name": "artist" + }, + { + "id": "c92e35fc-b634-437a-bfa0-47d15c67fe83", + "created_date": "2025-01-20 16:04:41.548000", + "last_modified_date": "2025-01-20 16:04:41.548000", + "version": 0, + "table_name": "module_data" + }, + { + "id": "c96112de-b521-4e92-8021-0172a1aebaf8", + "created_date": "2025-01-20 16:04:41.349000", + "last_modified_date": "2025-01-20 16:04:41.349000", + "version": 0, + "table_name": "bookshelf_publisher" + }, + { + "id": "cf9b16b9-1ec0-4bed-ba95-fef65013c069", + "created_date": "2025-01-20 16:04:41.377000", + "last_modified_date": "2025-01-20 16:04:41.377000", + "version": 0, + "table_name": "player" + }, + { + "id": "d7584a1a-0249-45ed-85e9-532680643296", + "created_date": "2025-01-20 16:04:41.561000", + "last_modified_date": "2025-01-20 16:04:41.561000", + "version": 0, + "table_name": "mail_account" + }, + { + "id": "d8b65435-dacc-4129-b08a-f4588110e127", + "created_date": "2025-01-20 16:04:41.502000", + "last_modified_date": "2025-01-20 16:04:41.502000", + "version": 0, + "table_name": "token" + }, + { + "id": "e4ab0e7b-4017-4ec0-9b33-b182a1850fda", + "created_date": "2025-01-20 16:04:41.199000", + "last_modified_date": "2025-01-20 16:04:41.199000", + "version": 0, + "table_name": "story_arc" + }, + { + "id": "e86c5386-a155-4f40-a6f5-e8da635d39c4", + "created_date": "2025-01-20 16:04:41.133000", + "last_modified_date": "2025-01-20 16:04:41.133000", + "version": 0, + "table_name": "issue" + }, + { + "id": "e962f787-7795-4179-8168-b2992981da12", + "created_date": "2025-01-20 16:04:41.332000", + "last_modified_date": "2025-01-20 16:04:41.332000", + "version": 0, + "table_name": "book_author" + }, + { + "id": "ede5536c-ca16-48ec-bd0d-a3b00cc1fcab", + "created_date": "2025-01-20 16:04:41.177000", + "last_modified_date": "2025-01-20 16:04:41.177000", + "version": 0, + "table_name": "trade_paperback" + }, + { + "id": "f99eb13a-b7c6-4f51-940e-2a2c36124a67", + "created_date": "2025-01-20 16:04:41.580000", + "last_modified_date": "2025-01-20 16:04:41.580000", + "version": 0, + "table_name": "mail" + } + ], + "volume": [], + "sport": [ + { + "id": "0718122d-8eea-4710-99cf-33a1f0a9c073", + "created_date": "2024-08-16 23:58:37.150000", + "last_modified_date": "2024-08-16 23:58:37.150000", + "version": 0, + "name": "Baseball" + }, + { + "id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e", + "created_date": "2024-08-16 23:58:37.154000", + "last_modified_date": "2024-08-16 23:58:37.154000", + "version": 0, + "name": "Hockey" + }, + { + "id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6", + "created_date": "2024-08-16 23:58:37.146000", + "last_modified_date": "2024-08-16 23:58:37.146000", + "version": 0, + "name": "Football" + }, + { + "id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb", + "created_date": "2024-08-16 23:58:37.152000", + "last_modified_date": "2024-08-16 23:58:37.152000", + "version": 0, + "name": "Basketball" + } + ], + "author": [ + { + "id": "c366e7c1-7475-4229-a69d-58ced52bfaf2", + "created_date": "2024-08-16 23:58:34.692000", + "last_modified_date": "2024-08-16 23:58:34.692000", + "version": 0, + "first_name": "Douglas", + "last_name": "Adams" + } + ], + "meta_data_column": [ + { + "id": "03ee0f74-1a99-47cc-92c0-afe5d7436d83", + "created_date": "2025-01-20 16:04:41.098000", + "last_modified_date": "2025-01-21 12:07:05.587000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "33b748ee-9f55-487a-a614-2ab64b844a13" + }, + { + "id": "0437399a-4327-43e3-a3b5-a2d69597bf6b", + "created_date": "2025-01-20 16:04:41.310000", + "last_modified_date": "2025-01-21 12:07:05.922000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "815b78c9-aef7-42b8-9f16-06a93a3761b3" + }, + { + "id": "0515448a-da3b-4ef1-96f0-4befccfe9868", + "created_date": "2025-01-20 16:04:41.163000", + "last_modified_date": "2025-01-21 12:07:05.685000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "13ea1a84-f92e-4ce6-b709-fe7c1893d229" + }, + { + "id": "05465284-7be3-40db-bae7-696d5a86fa60", + "created_date": "2025-01-20 16:04:41.328000", + "last_modified_date": "2025-01-21 12:07:05.941000", + "version": 1, + "column_name": "publisher_id", + "column_sync_name": "publisher_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 8, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "815b78c9-aef7-42b8-9f16-06a93a3761b3" + }, + { + "id": "055b3031-de10-4d50-8ca7-8784b65edebb", + "created_date": "2025-01-20 16:04:41.170000", + "last_modified_date": "2025-01-21 12:07:05.696000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "13ea1a84-f92e-4ce6-b709-fe7c1893d229" + }, + { + "id": "05cd24b6-f9d1-4ee6-b39c-4ce3e9b2a512", + "created_date": "2025-01-20 16:04:41.355000", + "last_modified_date": "2025-01-21 12:07:05.976000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "c96112de-b521-4e92-8021-0172a1aebaf8" + }, + { + "id": "06072c98-d51c-428f-a6c9-5d2ddca99a21", + "created_date": "2025-01-20 16:04:41.222000", + "last_modified_date": "2025-01-21 12:07:05.775000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "b7107df0-c08d-4370-9eff-8d7ebdea14ef" + }, + { + "id": "07069490-9d03-49d4-a4bb-eb43b71e65cf", + "created_date": "2025-01-20 16:04:41.341000", + "last_modified_date": "2025-01-21 12:07:05.957000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "e962f787-7795-4179-8168-b2992981da12" + }, + { + "id": "072c1188-bbe5-4e1c-bb8d-4fef313fdbf6", + "created_date": "2025-01-20 16:04:41.458000", + "last_modified_date": "2025-01-21 12:07:06.143000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "a7a24f23-586b-4ac4-97ab-b610e2ab2e5d" + }, + { + "id": "07eaf26b-4d49-4008-bf4f-5901b30c4d13", + "created_date": "2025-01-20 16:04:41.478000", + "last_modified_date": "2025-01-21 12:07:06.189000", + "version": 1, + "column_name": "card_set_id", + "column_sync_name": "card_set_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 7, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "727a04ab-67dd-4a80-b80c-d824e41bcda2" + }, + { + "id": "0896813d-8a00-41ca-8656-b9ce149897c9", + "created_date": "2025-01-20 16:04:41.228000", + "last_modified_date": "2025-01-21 12:07:05.783000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "b7107df0-c08d-4370-9eff-8d7ebdea14ef" + }, + { + "id": "089ea4df-d724-42da-a5d6-896aa4ba1df1", + "created_date": "2025-01-20 16:04:41.611000", + "last_modified_date": "2025-01-21 12:07:06.429000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" + }, + { + "id": "08ce1340-51b6-4556-99d1-9817637fe9ab", + "created_date": "2025-01-20 16:04:41.583000", + "last_modified_date": "2025-01-21 12:07:06.378000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "f99eb13a-b7c6-4f51-940e-2a2c36124a67" + }, + { + "id": "0a48118a-ca4f-4043-8d56-39a439af877f", + "created_date": "2025-01-20 16:04:41.122000", + "last_modified_date": "2025-01-21 12:07:05.626000", + "version": 1, + "column_name": "current_order", + "column_sync_name": "current_order", + "column_type": "BOOLEAN", + "column_modifier": null, + "column_order": 6, + "is_shown": 1, + "column_label": "Bestellung", + "show_filter": 1, + "filter_label": "Bestellung", + "ref_column": null, + "table_id": "26478de9-0bcc-427f-ba60-0a1ab79e107b" + }, + { + "id": "0a694da0-45ff-4604-826a-ed67a5a6b0c5", + "created_date": "2025-01-20 16:04:41.487000", + "last_modified_date": "2025-01-21 12:07:06.209000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c" + }, + { + "id": "0a87d4ec-fe51-484c-8203-3626af24edcc", + "created_date": "2025-01-20 16:04:41.267000", + "last_modified_date": "2025-01-21 12:07:05.856000", + "version": 1, + "column_name": "last_name", + "column_sync_name": "last_name", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 6, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "1951f2b6-3640-4568-9c47-a20e0768a843" + }, + { + "id": "0bbab597-06e3-417e-84a9-0a588d5854a3", + "created_date": "2025-01-20 16:04:40.988000", + "last_modified_date": "2025-01-21 12:07:05.319000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 1, + "column_label": "ID", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "63285ce5-eb45-47f5-9575-dec1f9778bbc" + }, + { + "id": "0bcbc19e-d442-429c-8640-ca8513c62fa6", + "created_date": "2025-01-20 16:04:41.462000", + "last_modified_date": "2025-01-21 12:07:06.154000", + "version": 1, + "column_name": "parallel_set", + "column_sync_name": "parallel_set", + "column_type": "BOOLEAN", + "column_modifier": null, + "column_order": 6, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "a7a24f23-586b-4ac4-97ab-b610e2ab2e5d" + }, + { + "id": "0c1c8451-d52d-4ef2-8b7b-fecd689bb702", + "created_date": "2025-01-20 16:04:41.109000", + "last_modified_date": "2025-01-21 12:07:05.602000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 1, + "column_label": "ID", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "26478de9-0bcc-427f-ba60-0a1ab79e107b" + }, + { + "id": "0d572856-2796-40e5-b6cf-05ffde91ee67", + "created_date": "2025-01-20 16:04:41.472000", + "last_modified_date": "2025-01-21 12:07:06.174000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "727a04ab-67dd-4a80-b80c-d824e41bcda2" + }, + { + "id": "0ea1f9d0-217d-4037-a9d6-d7624cc909ea", + "created_date": "2025-01-20 16:04:41.627000", + "last_modified_date": "2025-01-21 12:07:06.464000", + "version": 1, + "column_name": "filter_label", + "column_sync_name": "filter_label", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 13, + "is_shown": 1, + "column_label": "Filter Label", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" + }, + { + "id": "0f599c2f-f967-4869-9cce-7523e820017a", + "created_date": "2025-01-20 16:04:41.238000", + "last_modified_date": "2025-01-21 12:07:05.802000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "ae68f37b-9314-472c-86ea-387486f4db1c" + }, + { + "id": "1008557e-bf76-4b6e-bd25-62e025505ae0", + "created_date": "2025-01-20 16:04:41.026000", + "last_modified_date": "2025-01-21 12:07:05.452000", + "version": 1, + "column_name": "review", + "column_sync_name": "review", + "column_type": "BOOLEAN", + "column_modifier": null, + "column_order": 6, + "is_shown": 1, + "column_label": "Review", + "show_filter": 1, + "filter_label": "Review", + "ref_column": null, + "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" + }, + { + "id": "1034d208-8467-418a-9604-49d1047115a6", + "created_date": "2025-01-20 16:04:41.012000", + "last_modified_date": "2025-01-21 12:07:05.420000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 1, + "column_label": "ID", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" + }, + { + "id": "119dce89-209b-4557-92e0-9aab2d704358", + "created_date": "2025-01-20 16:04:41.343000", + "last_modified_date": "2025-01-21 12:07:05.960000", + "version": 1, + "column_name": "author_id", + "column_sync_name": "author_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "e962f787-7795-4179-8168-b2992981da12" + }, + { + "id": "1222fd06-05f8-48b4-9d8c-7f17b87153ab", + "created_date": "2025-01-20 16:04:41.590000", + "last_modified_date": "2025-01-21 12:07:06.391000", + "version": 1, + "column_name": "subject", + "column_sync_name": "subject", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 6, + "is_shown": 1, + "column_label": "Subject", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "f99eb13a-b7c6-4f51-940e-2a2c36124a67" + }, + { + "id": "14316703-5291-4b46-8cc2-0aa0d65ddc98", + "created_date": "2025-01-20 16:04:41.366000", + "last_modified_date": "2025-01-21 12:07:05.992000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "1850a718-dd07-4766-9856-591d311a4d0d" + }, + { + "id": "14eeb89e-ab64-4d0a-a079-ada87dba55b4", + "created_date": "2025-01-20 16:04:41.549000", + "last_modified_date": "2025-01-21 12:07:06.309000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 1, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "c92e35fc-b634-437a-bfa0-47d15c67fe83" + }, + { + "id": "15c5eaa1-03db-4b05-8931-906d8425e945", + "created_date": "2025-01-20 16:04:41.191000", + "last_modified_date": "2025-01-21 12:07:05.732000", + "version": 1, + "column_name": "issue_end", + "column_sync_name": "issue_end", + "column_type": "LONG", + "column_modifier": null, + "column_order": 6, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "ede5536c-ca16-48ec-bd0d-a3b00cc1fcab" + }, + { + "id": "179794a0-99b1-48c3-8ffd-e64d5fa9839b", + "created_date": "2025-01-20 16:04:41.368000", + "last_modified_date": "2025-01-21 12:07:05.996000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "1850a718-dd07-4766-9856-591d311a4d0d" + }, + { + "id": "17b378d0-ec86-481e-bd55-98f613895df1", + "created_date": "2025-01-20 16:04:41.616000", + "last_modified_date": "2025-01-21 12:07:06.442000", + "version": 1, + "column_name": "column_sync_name", + "column_sync_name": "column_sync_name", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 6, + "is_shown": 1, + "column_label": "SQLite Column", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" + }, + { + "id": "19dbe5fa-c37e-44a7-aa0a-8a514006b296", + "created_date": "2025-01-20 16:04:41.225000", + "last_modified_date": "2025-01-21 12:07:05.779000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "b7107df0-c08d-4370-9eff-8d7ebdea14ef" + }, + { + "id": "1a8abf9c-614b-4f0c-9ed4-bfd4e7763ac5", + "created_date": "2025-01-20 16:04:41.474000", + "last_modified_date": "2025-01-21 12:07:06.178000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "727a04ab-67dd-4a80-b80c-d824e41bcda2" + }, + { + "id": "1b97de61-a7a6-4254-a057-c5297e51b42f", + "created_date": "2025-01-20 16:04:41.434000", + "last_modified_date": "2025-01-21 12:07:06.099000", + "version": 1, + "column_name": "sport_id", + "column_sync_name": "sport_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 7, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "9a1a6de3-eae1-48e7-88c4-0c67908ec1bf" + }, + { + "id": "1d2628dd-d89a-4190-afc9-382fc47b9322", + "created_date": "2025-01-20 16:04:41.498000", + "last_modified_date": "2025-01-21 12:07:06.229000", + "version": 1, + "column_name": "password", + "column_sync_name": "password", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 9, + "is_shown": 0, + "column_label": "Password", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c" + }, + { + "id": "1e4609b3-3e47-41bd-a2e9-826a9db1bdb7", + "created_date": "2025-01-20 16:04:41.471000", + "last_modified_date": "2025-01-21 12:07:06.170000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "727a04ab-67dd-4a80-b80c-d824e41bcda2" + }, + { + "id": "20809d98-c875-49d3-b37c-993db1d1eb0e", + "created_date": "2025-01-20 16:04:41.500000", + "last_modified_date": "2025-01-21 12:07:06.233000", + "version": 1, + "column_name": "enabled", + "column_sync_name": "enabled", + "column_type": "BOOLEAN", + "column_modifier": null, + "column_order": 10, + "is_shown": 1, + "column_label": "", + "show_filter": 1, + "filter_label": null, + "ref_column": null, + "table_id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c" + }, + { + "id": "2144deeb-23f6-4f56-9407-6747c5df7d6c", + "created_date": "2025-01-20 16:04:41.402000", + "last_modified_date": "2025-01-21 12:07:06.048000", + "version": 1, + "column_name": "name", + "column_sync_name": "name", + "column_type": "TEXT", + "column_modifier": "UNIQUE", + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "a9170107-b28f-4476-947e-0aaa3a2a1e01" + }, + { + "id": "236f7302-a1af-4461-a147-2f23b9894910", + "created_date": "2025-01-20 16:04:41.279000", + "last_modified_date": "2025-01-21 12:07:05.871000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "721a49ce-a02d-4035-add9-126a4591cbb8" + }, + { + "id": "23fee615-37b2-4e69-846c-515b78aeef28", + "created_date": "2025-01-20 16:04:41.624000", + "last_modified_date": "2025-01-21 12:07:06.458000", + "version": 1, + "column_name": "column_label", + "column_sync_name": "column_label", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 11, + "is_shown": 1, + "column_label": "Label", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" + }, + { + "id": "261eb1f8-d845-44a8-a6a9-70957f893409", + "created_date": "2025-01-20 16:04:41.427000", + "last_modified_date": "2025-01-21 12:07:06.086000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "9a1a6de3-eae1-48e7-88c4-0c67908ec1bf" + }, + { + "id": "27a6fceb-dc77-4a9c-b897-8cc11bead54a", + "created_date": "2025-01-20 16:04:41.568000", + "last_modified_date": "2025-01-21 12:07:06.344000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "d7584a1a-0249-45ed-85e9-532680643296" + }, + { + "id": "2a601ddc-6fbe-4402-8033-ce42adf0a4fa", + "created_date": "2025-01-20 16:04:41.244000", + "last_modified_date": "2025-01-21 12:07:05.812000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "ae68f37b-9314-472c-86ea-387486f4db1c" + }, + { + "id": "2ea9583e-c19f-4692-a2dd-578ad5b6553d", + "created_date": "2025-01-20 16:04:41.250000", + "last_modified_date": "2025-01-21 12:07:05.824000", + "version": 1, + "column_name": "comic_id", + "column_sync_name": "comic_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 6, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "ae68f37b-9314-472c-86ea-387486f4db1c" + }, + { + "id": "2fd476b2-0df4-4b12-836e-babfcacb28e4", + "created_date": "2025-01-20 16:04:41.334000", + "last_modified_date": "2025-01-21 12:07:05.947000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "e962f787-7795-4179-8168-b2992981da12" + }, + { + "id": "321270c6-366d-4713-9f92-f78dc3e903ce", + "created_date": "2025-01-20 16:04:41.388000", + "last_modified_date": "2025-01-21 12:07:06.026000", + "version": 1, + "column_name": "first_name", + "column_sync_name": "first_name", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "cf9b16b9-1ec0-4bed-ba95-fef65013c069" + }, + { + "id": "3334c5c2-cf01-459b-b021-67cfcad9dc40", + "created_date": "2025-01-20 16:04:41.578000", + "last_modified_date": "2025-01-21 12:07:06.369000", + "version": 1, + "column_name": "start_tls", + "column_sync_name": "start_tls", + "column_type": "BOOLEAN", + "column_modifier": null, + "column_order": 10, + "is_shown": 1, + "column_label": "StartTLS", + "show_filter": 1, + "filter_label": "StartTLS", + "ref_column": null, + "table_id": "d7584a1a-0249-45ed-85e9-532680643296" + }, + { + "id": "3351c7ff-bf86-41c7-b426-c90a071601fc", + "created_date": "2025-01-20 16:04:40.997000", + "last_modified_date": "2025-01-21 12:07:05.386000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "63285ce5-eb45-47f5-9575-dec1f9778bbc" + }, + { + "id": "3453a28e-1fad-45b7-bec9-e125b1d70ef5", + "created_date": "2025-01-20 16:04:41.231000", + "last_modified_date": "2025-01-21 12:07:05.795000", + "version": 1, + "column_name": "name", + "column_sync_name": "name", + "column_type": "TEXT", + "column_modifier": "UNIQUE", + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "b7107df0-c08d-4370-9eff-8d7ebdea14ef" + }, + { + "id": "34d54638-7820-4943-9026-a685a5c65942", + "created_date": "2025-01-20 16:04:41.564000", + "last_modified_date": "2025-01-21 12:07:06.337000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "d7584a1a-0249-45ed-85e9-532680643296" + }, + { + "id": "355a64ce-5196-45cd-9915-c656a96ed2c6", + "created_date": "2025-01-20 16:04:41.339000", + "last_modified_date": "2025-01-21 12:07:05.954000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "e962f787-7795-4179-8168-b2992981da12" + }, + { + "id": "361c9a8d-2a9a-4662-97f6-612dd90eedb8", + "created_date": "2025-01-20 16:04:41.602000", + "last_modified_date": "2025-01-21 12:07:06.414000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "058fa05e-2dab-4f61-8415-b5beec1fe79e" + }, + { + "id": "365ce980-90ba-4d3e-befd-956467ce3f54", + "created_date": "2025-01-20 16:04:41.609000", + "last_modified_date": "2025-01-21 12:07:06.426000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" + }, + { + "id": "36f94e1b-601d-42f7-90c0-c19f4bf63d91", + "created_date": "2025-01-20 16:04:41.492000", + "last_modified_date": "2025-01-21 12:07:06.212000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c" + }, + { + "id": "3881219c-8329-4f1c-b81d-86b97a3ba0b2", + "created_date": "2025-01-20 16:04:41.485000", + "last_modified_date": "2025-01-21 12:07:06.202000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 1, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c" + }, + { + "id": "398c17e5-a694-408c-81df-beaedad99486", + "created_date": "2025-01-20 16:04:41.538000", + "last_modified_date": "2025-01-21 12:07:06.287000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 1, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "36c7ac9c-270d-40a4-af43-c952120f165c" + }, + { + "id": "3bf5a99b-3d42-4e7f-958f-1abe357d8fcb", + "created_date": "2025-01-20 16:04:41.068000", + "last_modified_date": "2025-01-21 12:07:05.535000", + "version": 1, + "column_name": "file_name", + "column_sync_name": "file_name", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 9, + "is_shown": 1, + "column_label": "Dateiname", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" + }, + { + "id": "3c5e32ae-8b77-4173-8f80-fa458da7f8b0", + "created_date": "2025-01-20 16:04:41.072000", + "last_modified_date": "2025-01-21 12:07:05.545000", + "version": 1, + "column_name": "cloud_link", + "column_sync_name": "cloud_link", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 11, + "is_shown": 1, + "column_label": "Cloud Link", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" + }, + { + "id": "3d249be9-3990-451d-8665-03855a3b06c5", + "created_date": "2025-01-20 16:04:41.413000", + "last_modified_date": "2025-01-21 12:07:06.063000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "3773ac76-a957-42b2-b24d-5e66e4c32c12" + }, + { + "id": "3dc0ea2d-7367-4b65-804a-4e63758c09a7", + "created_date": "2025-01-20 16:04:41.180000", + "last_modified_date": "2025-01-21 12:07:05.710000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "ede5536c-ca16-48ec-bd0d-a3b00cc1fcab" + }, + { + "id": "41673309-d4b3-44fc-a7b0-7f13d535d49f", + "created_date": "2025-01-20 16:04:41.142000", + "last_modified_date": "2025-01-21 12:07:05.652000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "e86c5386-a155-4f40-a6f5-e8da635d39c4" + }, + { + "id": "41ce422a-fa89-4b0c-8e50-d28cbe8a285f", + "created_date": "2025-01-20 16:04:41.599000", + "last_modified_date": "2025-01-21 12:07:06.408000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 1, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "058fa05e-2dab-4f61-8415-b5beec1fe79e" + }, + { + "id": "4247146c-8911-441e-aac9-ef1bcb73eb07", + "created_date": "2025-01-20 16:04:41.174000", + "last_modified_date": "2025-01-21 12:07:05.704000", + "version": 1, + "column_name": "comic_id", + "column_sync_name": "comic_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 6, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "13ea1a84-f92e-4ce6-b709-fe7c1893d229" + }, + { + "id": "42c4d72b-742e-4692-9797-c94e5f5b91bf", + "created_date": "2025-01-20 16:04:41.201000", + "last_modified_date": "2025-01-21 12:07:05.746000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "e4ab0e7b-4017-4ec0-9b33-b182a1850fda" + }, + { + "id": "432204a9-1f00-4a4f-ad8c-fe051ab84299", + "created_date": "2025-01-20 16:04:41.150000", + "last_modified_date": "2025-01-21 12:07:05.665000", + "version": 1, + "column_name": "is_read", + "column_sync_name": "is_read", + "column_type": "BOOLEAN", + "column_modifier": null, + "column_order": 6, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "e86c5386-a155-4f40-a6f5-e8da635d39c4" + }, + { + "id": "43d855fc-15e2-4a87-b7b3-3c43f0091f9f", + "created_date": "2025-01-20 16:04:41.540000", + "last_modified_date": "2025-01-21 12:07:06.291000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "36c7ac9c-270d-40a4-af43-c952120f165c" + }, + { + "id": "4488149c-3dba-43c1-a925-101c42fbd7cf", + "created_date": "2025-01-20 16:04:41.085000", + "last_modified_date": "2025-01-21 12:07:05.568000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "c9060971-3f7d-4eb1-89e9-bb40d8687e77" + }, + { + "id": "46db6f22-7ae4-477f-ad66-e0e8783bdd8e", + "created_date": "2025-01-20 16:04:41.001000", + "last_modified_date": "2025-01-21 12:07:05.398000", + "version": 1, + "column_name": "url", + "column_sync_name": "link_url", + "column_type": "TEXT", + "column_modifier": "UNIQUE", + "column_order": 5, + "is_shown": 1, + "column_label": "URL", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "63285ce5-eb45-47f5-9575-dec1f9778bbc" + }, + { + "id": "47283da8-4f49-4383-a755-2dad75ceaa6e", + "created_date": "2025-01-20 16:04:41.325000", + "last_modified_date": "2025-01-21 12:07:05.938000", + "version": 1, + "column_name": "year", + "column_sync_name": "year", + "column_type": "LONG", + "column_modifier": null, + "column_order": 7, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "815b78c9-aef7-42b8-9f16-06a93a3761b3" + }, + { + "id": "47f5e390-9bcf-4b3d-9470-69dac67bd504", + "created_date": "2025-01-20 16:04:41.261000", + "last_modified_date": "2025-01-21 12:07:05.843000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "1951f2b6-3640-4568-9c47-a20e0768a843" + }, + { + "id": "48032d74-a203-416b-b8a4-35e005fc5cb1", + "created_date": "2025-01-20 16:04:41.476000", + "last_modified_date": "2025-01-21 12:07:06.185000", + "version": 1, + "column_name": "year", + "column_sync_name": "year", + "column_type": "LONG", + "column_modifier": null, + "column_order": 6, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "727a04ab-67dd-4a80-b80c-d824e41bcda2" + }, + { + "id": "487fae4f-0987-4c64-8c8a-44b96c9e1ace", + "created_date": "2025-01-20 16:04:41.274000", + "last_modified_date": "2025-01-21 12:07:05.864000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "721a49ce-a02d-4035-add9-126a4591cbb8" + }, + { + "id": "48e670ae-95de-402d-a800-6e568ab0e12b", + "created_date": "2025-01-20 16:04:41.052000", + "last_modified_date": "2025-01-21 12:07:05.499000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "Created", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" + }, + { + "id": "497a41e6-305e-48a4-baf4-f0d86feb33e9", + "created_date": "2025-01-20 16:04:41.056000", + "last_modified_date": "2025-01-21 12:07:05.509000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "Version", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" + }, + { + "id": "49845f27-8d26-4dd2-b139-c64d1d6e5147", + "created_date": "2025-01-20 16:04:41.527000", + "last_modified_date": "2025-01-21 12:07:06.273000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "925989d3-faa9-4bb4-9183-3564b29063ec" + }, + { + "id": "498559d5-13cf-4ef9-b0bc-9304ac84a352", + "created_date": "2025-01-20 16:04:41.596000", + "last_modified_date": "2025-01-21 12:07:06.402000", + "version": 1, + "column_name": "received_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 9, + "is_shown": 0, + "column_label": "Empfangen", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "f99eb13a-b7c6-4f51-940e-2a2c36124a67" + }, + { + "id": "49bf7ec6-2583-4d7e-a91c-2971dd3a7dd6", + "created_date": "2025-01-20 16:04:41.442000", + "last_modified_date": "2025-01-21 12:07:06.108000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "3ce78ef2-35c6-4397-8cdf-9241b52236df" + }, + { + "id": "4a414632-df81-41c2-b85d-70ec2f71dcac", + "created_date": "2025-01-20 16:04:41.615000", + "last_modified_date": "2025-01-21 12:07:06.438000", + "version": 1, + "column_name": "column_name", + "column_sync_name": "column_name", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 5, + "is_shown": 1, + "column_label": "Column", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" + }, + { + "id": "4a9506f7-4fc7-45ed-b570-d7f32bfe2dcc", + "created_date": "2025-01-20 16:04:41.466000", + "last_modified_date": "2025-01-21 12:07:06.161000", + "version": 1, + "column_name": "vendor_id", + "column_sync_name": "vendor_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 8, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "a7a24f23-586b-4ac4-97ab-b610e2ab2e5d" + }, + { + "id": "4b442acc-3d9b-4d4e-906b-49fb1de52e4e", + "created_date": "2025-01-20 16:04:40.999000", + "last_modified_date": "2025-01-21 12:07:05.392000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "63285ce5-eb45-47f5-9575-dec1f9778bbc" + }, + { + "id": "4db2fa33-64e6-4596-a1b2-9fdc657dd3b3", + "created_date": "2025-01-20 16:04:41.423000", + "last_modified_date": "2025-01-21 12:07:06.079000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "9a1a6de3-eae1-48e7-88c4-0c67908ec1bf" + }, + { + "id": "4dc7467e-8707-46fe-9478-071c7006b567", + "created_date": "2025-01-20 16:04:41.252000", + "last_modified_date": "2025-01-21 12:07:05.829000", + "version": 1, + "column_name": "work_type_id", + "column_sync_name": "work_type_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 7, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "ae68f37b-9314-472c-86ea-387486f4db1c" + }, + { + "id": "4dd473c3-1850-4c0a-90ca-34084cf17420", + "created_date": "2025-01-20 16:04:41.042000", + "last_modified_date": "2025-01-21 12:07:05.484000", + "version": 1, + "column_name": "cloud_link", + "column_sync_name": "cloud_link", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 11, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" + }, + { + "id": "4e3940e5-b899-4667-a6f5-cc206cf030d8", + "created_date": "2025-01-20 16:04:41.507000", + "last_modified_date": "2025-01-21 12:07:06.245000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "d8b65435-dacc-4129-b08a-f4588110e127" + }, + { + "id": "511620ef-4bce-4d28-bcfa-5d94a1b979ba", + "created_date": "2025-01-20 16:04:41.298000", + "last_modified_date": "2025-01-21 12:07:05.903000", + "version": 1, + "column_name": "article_id", + "column_sync_name": "article_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "7ebdc79a-d0a7-42dc-9e50-d93c79e182af" + }, + { + "id": "5276d5cc-94a7-4840-ab2a-5daec758d7b2", + "created_date": "2025-01-20 16:04:41.005000", + "last_modified_date": "2025-01-21 12:07:05.410000", + "version": 1, + "column_name": "title", + "column_sync_name": "title", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 7, + "is_shown": 1, + "column_label": "Title", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "63285ce5-eb45-47f5-9575-dec1f9778bbc" + }, + { + "id": "52fd2cc1-d77d-4fbd-bde9-55c522de4a46", + "created_date": "2025-01-20 16:04:41.308000", + "last_modified_date": "2025-01-21 12:07:05.917000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "815b78c9-aef7-42b8-9f16-06a93a3761b3" + }, + { + "id": "55cb2c7f-b93e-4cbe-9886-6f035ab57a5f", + "created_date": "2025-01-20 16:04:41.152000", + "last_modified_date": "2025-01-21 12:07:05.669000", + "version": 1, + "column_name": "issue_number", + "column_sync_name": "issue_number", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 7, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "e86c5386-a155-4f40-a6f5-e8da635d39c4" + }, + { + "id": "568d6d6a-f9df-40a6-8553-17480040a2fa", + "created_date": "2025-01-20 16:04:41.518000", + "last_modified_date": "2025-01-21 12:07:06.263000", + "version": 1, + "column_name": "enabled", + "column_sync_name": "enabled", + "column_type": "BOOLEAN", + "column_modifier": null, + "column_order": 8, + "is_shown": 1, + "column_label": "", + "show_filter": 1, + "filter_label": "Enabled", + "ref_column": null, + "table_id": "d8b65435-dacc-4129-b08a-f4588110e127" + }, + { + "id": "56f983ed-4d38-4a2f-89b8-c042aeeef043", + "created_date": "2025-01-20 16:04:41.440000", + "last_modified_date": "2025-01-21 12:07:06.104000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "3ce78ef2-35c6-4397-8cdf-9241b52236df" + }, + { + "id": "57c6ac6b-50f7-40f1-b809-fcafaae5bb34", + "created_date": "2025-01-20 16:04:41.604000", + "last_modified_date": "2025-01-21 12:07:06.420000", + "version": 1, + "column_name": "table_name", + "column_sync_name": "table_name", + "column_type": "TEXT", + "column_modifier": "UNIQUE", + "column_order": 5, + "is_shown": 1, + "column_label": "Table", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "058fa05e-2dab-4f61-8415-b5beec1fe79e" + }, + { + "id": "57e8ebb5-b8a7-49fb-b74a-2bf1e65f4cb6", + "created_date": "2025-01-20 16:04:41.219000", + "last_modified_date": "2025-01-21 12:07:05.772000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "b7107df0-c08d-4370-9eff-8d7ebdea14ef" + }, + { + "id": "581501f7-9dc5-44df-b1a3-74f930db7c1b", + "created_date": "2025-01-20 16:04:41.373000", + "last_modified_date": "2025-01-21 12:07:06.005000", + "version": 1, + "column_name": "name", + "column_sync_name": "name", + "column_type": "TEXT", + "column_modifier": "UNIQUE", + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "1850a718-dd07-4766-9856-591d311a4d0d" + }, + { + "id": "5a97f0ea-c91c-49ae-b682-86807aedfdd7", + "created_date": "2025-01-20 16:04:41.455000", + "last_modified_date": "2025-01-21 12:07:06.136000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "a7a24f23-586b-4ac4-97ab-b610e2ab2e5d" + }, + { + "id": "5aeb10c3-8732-46a7-8a13-9385a183ba44", + "created_date": "2025-01-20 16:04:41.188000", + "last_modified_date": "2025-01-21 12:07:05.728000", + "version": 1, + "column_name": "issue_start", + "column_sync_name": "issue_start", + "column_type": "LONG", + "column_modifier": null, + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "ede5536c-ca16-48ec-bd0d-a3b00cc1fcab" + }, + { + "id": "5c99ea74-f722-4c0e-8148-ff9d9684570a", + "created_date": "2025-01-20 16:04:41.525000", + "last_modified_date": "2025-01-21 12:07:06.271000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 1, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "925989d3-faa9-4bb4-9183-3564b29063ec" + }, + { + "id": "5dec99e7-0066-41cb-aa8e-fd5abaa3622d", + "created_date": "2025-01-20 16:04:41.603000", + "last_modified_date": "2025-01-21 12:07:06.417000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "058fa05e-2dab-4f61-8415-b5beec1fe79e" + }, + { + "id": "5eb89fb4-636c-4721-a931-fd4ed7940d10", + "created_date": "2025-01-20 16:04:41.591000", + "last_modified_date": "2025-01-21 12:07:06.395000", + "version": 1, + "column_name": "body", + "column_sync_name": "body", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 7, + "is_shown": 0, + "column_label": "Body", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "f99eb13a-b7c6-4f51-940e-2a2c36124a67" + }, + { + "id": "5f18a8a4-dca2-4eed-b45c-bd8b2013cfd3", + "created_date": "2025-01-20 16:04:41.066000", + "last_modified_date": "2025-01-21 12:07:05.530000", + "version": 1, + "column_name": "title", + "column_sync_name": "title", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 8, + "is_shown": 1, + "column_label": "Title", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" + }, + { + "id": "5f267ab0-669b-4544-aad8-8a9dd0dbd73b", + "created_date": "2025-01-20 16:04:41.022000", + "last_modified_date": "2025-01-21 12:07:05.446000", + "version": 1, + "column_name": "url", + "column_sync_name": "link_url", + "column_type": "TEXT", + "column_modifier": "UNIQUE", + "column_order": 5, + "is_shown": 1, + "column_label": "URL", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" + }, + { + "id": "608efc2a-4d08-4dd4-96c9-bacc37314dba", + "created_date": "2025-01-20 16:04:41.406000", + "last_modified_date": "2025-01-21 12:07:06.054000", + "version": 1, + "column_name": "sport_id", + "column_sync_name": "sport_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 7, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "a9170107-b28f-4476-947e-0aaa3a2a1e01" + }, + { + "id": "616ee1c5-3da5-48a3-baa0-d7160b8db180", + "created_date": "2025-01-20 16:04:41.306000", + "last_modified_date": "2025-01-21 12:07:05.913000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "815b78c9-aef7-42b8-9f16-06a93a3761b3" + }, + { + "id": "623d3bf2-1615-4331-ad79-13bf523cc154", + "created_date": "2025-01-20 16:04:41.138000", + "last_modified_date": "2025-01-21 12:07:05.647000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "e86c5386-a155-4f40-a6f5-e8da635d39c4" + }, + { + "id": "6295b2f3-72e7-43b4-aed7-93d3c6ad86be", + "created_date": "2025-01-20 16:04:41.625000", + "last_modified_date": "2025-01-21 12:07:06.461000", + "version": 1, + "column_name": "show_filter", + "column_sync_name": "show_filter", + "column_type": "BOOLEAN", + "column_modifier": null, + "column_order": 12, + "is_shown": 1, + "column_label": "Show Filter", + "show_filter": 1, + "filter_label": "Show Filter", + "ref_column": null, + "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" + }, + { + "id": "62aa6fe8-1111-4597-af70-db709553fb3c", + "created_date": "2025-01-20 16:04:41.432000", + "last_modified_date": "2025-01-21 12:07:06.095000", + "version": 1, + "column_name": "short_name", + "column_sync_name": "short_name", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 6, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "9a1a6de3-eae1-48e7-88c4-0c67908ec1bf" + }, + { + "id": "634ecab0-a1a4-4baa-8437-af4522b51ef1", + "created_date": "2025-01-20 16:04:41.165000", + "last_modified_date": "2025-01-21 12:07:05.689000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "13ea1a84-f92e-4ce6-b709-fe7c1893d229" + }, + { + "id": "63991f2e-49ff-4293-a8d6-b1287bb80975", + "created_date": "2025-01-20 16:04:41.400000", + "last_modified_date": "2025-01-21 12:07:06.045000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "a9170107-b28f-4476-947e-0aaa3a2a1e01" + }, + { + "id": "63e5d2bc-dbca-4731-8f0e-5829749e1fe4", + "created_date": "2025-01-20 16:04:41.353000", + "last_modified_date": "2025-01-21 12:07:05.973000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "c96112de-b521-4e92-8021-0172a1aebaf8" + }, + { + "id": "6422a660-c56b-4aa8-a371-7ace8f659e5c", + "created_date": "2025-01-20 16:04:41.364000", + "last_modified_date": "2025-01-21 12:07:05.988000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "1850a718-dd07-4766-9856-591d311a4d0d" + }, + { + "id": "6644fc7d-ce34-427c-bd32-37d2df93a9af", + "created_date": "2025-01-20 16:04:41.322000", + "last_modified_date": "2025-01-21 12:07:05.933000", + "version": 1, + "column_name": "title", + "column_sync_name": "title", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 6, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "815b78c9-aef7-42b8-9f16-06a93a3761b3" + }, + { + "id": "66a6de00-e088-4ece-a223-a37193c51aca", + "created_date": "2025-01-20 16:04:41.315000", + "last_modified_date": "2025-01-21 12:07:05.930000", + "version": 1, + "column_name": "isbn", + "column_sync_name": "isbn", + "column_type": "TEXT", + "column_modifier": "UNIQUE", + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "815b78c9-aef7-42b8-9f16-06a93a3761b3" + }, + { + "id": "682c7c9c-843e-4b2c-8e1e-1973e1ffbe5d", + "created_date": "2025-01-20 16:04:41.404000", + "last_modified_date": "2025-01-21 12:07:06.051000", + "version": 1, + "column_name": "short_name", + "column_sync_name": "short_name", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 6, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "a9170107-b28f-4476-947e-0aaa3a2a1e01" + }, + { + "id": "686e4b0a-b5ab-473a-8e0b-b4401e196039", + "created_date": "2025-01-20 16:04:41.532000", + "last_modified_date": "2025-01-21 12:07:06.282000", + "version": 1, + "column_name": "name", + "column_sync_name": "name", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 5, + "is_shown": 1, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "925989d3-faa9-4bb4-9183-3564b29063ec" + }, + { + "id": "6a46aa6b-749e-4985-b491-0b06cb78e77b", + "created_date": "2025-01-20 16:04:41.102000", + "last_modified_date": "2025-01-21 12:07:05.595000", + "version": 1, + "column_name": "name", + "column_sync_name": "name", + "column_type": "TEXT", + "column_modifier": "UNIQUE", + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "33b748ee-9f55-487a-a614-2ab64b844a13" + }, + { + "id": "6a974ed1-b29d-4f6f-a309-bcb8ede29430", + "created_date": "2025-01-20 16:04:41.554000", + "last_modified_date": "2025-01-21 12:07:06.321000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "c92e35fc-b634-437a-bfa0-47d15c67fe83" + }, + { + "id": "6c46f5e1-0d9f-4706-bcf3-2c3678c5857b", + "created_date": "2025-01-20 16:04:41.546000", + "last_modified_date": "2025-01-21 12:07:06.305000", + "version": 1, + "column_name": "role_id", + "column_sync_name": "role_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 6, + "is_shown": 1, + "column_label": "Role", + "show_filter": 0, + "filter_label": null, + "ref_column": "name", + "table_id": "36c7ac9c-270d-40a4-af43-c952120f165c" + }, + { + "id": "6cc594e1-851b-4a0f-b124-47fe6b349dcf", + "created_date": "2025-01-20 16:04:41.114000", + "last_modified_date": "2025-01-21 12:07:05.611000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "26478de9-0bcc-427f-ba60-0a1ab79e107b" + }, + { + "id": "6cd374d1-610f-4924-b767-c139a6582ca4", + "created_date": "2025-01-20 16:04:41.312000", + "last_modified_date": "2025-01-21 12:07:05.925000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "815b78c9-aef7-42b8-9f16-06a93a3761b3" + }, + { + "id": "6d22c2b2-5aab-4f1c-a18b-8dc29fbff475", + "created_date": "2025-01-20 16:04:41.246000", + "last_modified_date": "2025-01-21 12:07:05.816000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "ae68f37b-9314-472c-86ea-387486f4db1c" + }, + { + "id": "6e5e1357-cb14-499b-a624-20ceb6a363f4", + "created_date": "2025-01-20 16:04:41.116000", + "last_modified_date": "2025-01-21 12:07:05.616000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "26478de9-0bcc-427f-ba60-0a1ab79e107b" + }, + { + "id": "7049b64d-8c7a-4341-b57e-556b28851c07", + "created_date": "2025-01-20 16:04:41.019000", + "last_modified_date": "2025-01-21 12:07:05.439000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "Version", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" + }, + { + "id": "7267cc8c-5aaa-4f40-a42a-c075a9aed8a6", + "created_date": "2025-01-20 16:04:41.082000", + "last_modified_date": "2025-01-21 12:07:05.563000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "c9060971-3f7d-4eb1-89e9-bb40d8687e77" + }, + { + "id": "7404eb62-541f-4a56-8040-1a99b6278a0c", + "created_date": "2025-01-20 16:04:41.078000", + "last_modified_date": "2025-01-21 12:07:05.554000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "c9060971-3f7d-4eb1-89e9-bb40d8687e77" + }, + { + "id": "74afd75d-90d0-47de-9d7a-63d64dde3119", + "created_date": "2025-01-20 16:04:41.617000", + "last_modified_date": "2025-01-21 12:07:06.446000", + "version": 1, + "column_name": "column_type", + "column_sync_name": "column_type", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 7, + "is_shown": 1, + "column_label": "Type", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" + }, + { + "id": "75c2550b-68da-41f6-a301-19c4f199750b", + "created_date": "2025-01-20 16:04:41.125000", + "last_modified_date": "2025-01-21 12:07:05.630000", + "version": 1, + "column_name": "title", + "column_sync_name": "title", + "column_type": "TEXT", + "column_modifier": "UNIQUE", + "column_order": 7, + "is_shown": 1, + "column_label": "Title", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "26478de9-0bcc-427f-ba60-0a1ab79e107b" + }, + { + "id": "785fb5fd-41b0-41a5-8bcf-89162d0abaf7", + "created_date": "2025-01-20 16:04:41.516000", + "last_modified_date": "2025-01-21 12:07:06.260000", + "version": 1, + "column_name": "last_used_date", + "column_sync_name": "used", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 7, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "d8b65435-dacc-4129-b08a-f4588110e127" + }, + { + "id": "7bcc1735-5eb8-4b2a-8921-12918dbbb793", + "created_date": "2025-01-20 16:04:41.582000", + "last_modified_date": "2025-01-21 12:07:06.374000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "f99eb13a-b7c6-4f51-940e-2a2c36124a67" + }, + { + "id": "7bde6aed-6d33-428d-8881-fab387298852", + "created_date": "2025-01-20 16:04:41.497000", + "last_modified_date": "2025-01-21 12:07:06.226000", + "version": 1, + "column_name": "email", + "column_sync_name": "email", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 8, + "is_shown": 1, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c" + }, + { + "id": "7ca4ea1b-1ed6-4f1a-a55d-9ea47b467678", + "created_date": "2025-01-20 16:04:41.475000", + "last_modified_date": "2025-01-21 12:07:06.182000", + "version": 1, + "column_name": "card_number", + "column_sync_name": "card_number", + "column_type": "LONG", + "column_modifier": null, + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "727a04ab-67dd-4a80-b80c-d824e41bcda2" + }, + { + "id": "7decae7f-fbf1-4cbb-8ed4-907bbc582141", + "created_date": "2025-01-20 16:04:41.425000", + "last_modified_date": "2025-01-21 12:07:06.083000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "9a1a6de3-eae1-48e7-88c4-0c67908ec1bf" + }, + { + "id": "7df0e865-9888-41b4-a407-c7d5d88a6965", + "created_date": "2025-01-20 16:04:41.095000", + "last_modified_date": "2025-01-21 12:07:05.583000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "33b748ee-9f55-487a-a614-2ab64b844a13" + }, + { + "id": "7e30dfe6-6a6a-48da-8c15-d6464f77cdfe", + "created_date": "2025-01-20 16:04:41.284000", + "last_modified_date": "2025-01-21 12:07:05.879000", + "version": 1, + "column_name": "title", + "column_sync_name": "title", + "column_type": "TEXT", + "column_modifier": "UNIQUE", + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "721a49ce-a02d-4035-add9-126a4591cbb8" + }, + { + "id": "7ea438b6-eb65-4c1a-a270-cbd3e6e3f8d9", + "created_date": "2025-01-20 16:04:41.429000", + "last_modified_date": "2025-01-21 12:07:06.089000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "9a1a6de3-eae1-48e7-88c4-0c67908ec1bf" + }, + { + "id": "7f1861b0-f1b2-4c28-8f5a-d43e8411afd8", + "created_date": "2025-01-20 16:04:41.496000", + "last_modified_date": "2025-01-21 12:07:06.223000", + "version": 1, + "column_name": "user_name", + "column_sync_name": "user_name", + "column_type": "TEXT", + "column_modifier": "UNIQUE", + "column_order": 7, + "is_shown": 1, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c" + }, + { + "id": "7f39bac1-7d9c-42b2-9b54-701242220258", + "created_date": "2025-01-20 16:04:40.994000", + "last_modified_date": "2025-01-21 12:07:05.379000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "63285ce5-eb45-47f5-9575-dec1f9778bbc" + }, + { + "id": "7f54d6da-9548-49d5-b064-8a5c47ac69b9", + "created_date": "2025-01-20 16:04:41.480000", + "last_modified_date": "2025-01-21 12:07:06.197000", + "version": 1, + "column_name": "vendor_id", + "column_sync_name": "vendor_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 9, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "727a04ab-67dd-4a80-b80c-d824e41bcda2" + }, + { + "id": "80a0742a-52d1-4014-808f-85eace053174", + "created_date": "2025-01-20 16:04:41.381000", + "last_modified_date": "2025-01-21 12:07:06.014000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "cf9b16b9-1ec0-4bed-ba95-fef65013c069" + }, + { + "id": "80de3ee1-1838-41c1-af2a-2f62d7312051", + "created_date": "2025-01-20 16:04:41.203000", + "last_modified_date": "2025-01-21 12:07:05.751000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "e4ab0e7b-4017-4ec0-9b33-b182a1850fda" + }, + { + "id": "82a99e30-a35c-4bba-b535-f2ebb9c19ca4", + "created_date": "2025-01-20 16:04:41.574000", + "last_modified_date": "2025-01-21 12:07:06.361000", + "version": 1, + "column_name": "user_name", + "column_sync_name": "user_name", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 8, + "is_shown": 1, + "column_label": "Username", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "d7584a1a-0249-45ed-85e9-532680643296" + }, + { + "id": "8495a964-57f1-4047-a95f-b8a27ba723c0", + "created_date": "2025-01-20 16:04:41.444000", + "last_modified_date": "2025-01-21 12:07:06.112000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "3ce78ef2-35c6-4397-8cdf-9241b52236df" + }, + { + "id": "853836ab-36ba-41d3-93f5-a85d56f95c76", + "created_date": "2025-01-20 16:04:41.186000", + "last_modified_date": "2025-01-21 12:07:05.723000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "ede5536c-ca16-48ec-bd0d-a3b00cc1fcab" + }, + { + "id": "85575a50-9a98-4a30-be18-300ed6325c93", + "created_date": "2025-01-20 16:04:41.600000", + "last_modified_date": "2025-01-21 12:07:06.411000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "058fa05e-2dab-4f61-8415-b5beec1fe79e" + }, + { + "id": "8591dd8b-21fd-45d1-8307-0299bee2858f", + "created_date": "2025-01-20 16:04:41.395000", + "last_modified_date": "2025-01-21 12:07:06.035000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "a9170107-b28f-4476-947e-0aaa3a2a1e01" + }, + { + "id": "8f8d7d86-894b-4806-8faf-9405a4e30c3f", + "created_date": "2025-01-20 16:04:41.457000", + "last_modified_date": "2025-01-21 12:07:06.139000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "a7a24f23-586b-4ac4-97ab-b610e2ab2e5d" + }, + { + "id": "90b0e104-cad3-4dd9-8bb2-ddb8abfef922", + "created_date": "2025-01-20 16:04:41.351000", + "last_modified_date": "2025-01-21 12:07:05.969000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "c96112de-b521-4e92-8021-0172a1aebaf8" + }, + { + "id": "9660d331-cbbf-4f4e-97c0-a6ba6160de6c", + "created_date": "2025-01-20 16:04:41.061000", + "last_modified_date": "2025-01-21 12:07:05.520000", + "version": 1, + "column_name": "review", + "column_sync_name": "review", + "column_type": "BOOLEAN", + "column_modifier": null, + "column_order": 6, + "is_shown": 1, + "column_label": "Review", + "show_filter": 1, + "filter_label": "Review", + "ref_column": null, + "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" + }, + { + "id": "9726256c-8951-4e7b-b6a3-2fe2722ef241", + "created_date": "2025-01-20 16:04:41.459000", + "last_modified_date": "2025-01-21 12:07:06.146000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "a7a24f23-586b-4ac4-97ab-b610e2ab2e5d" + }, + { + "id": "9769122a-ea9c-4b82-861b-2e0b7c701a11", + "created_date": "2025-01-20 16:04:41.155000", + "last_modified_date": "2025-01-21 12:07:05.674000", + "version": 1, + "column_name": "comic_id", + "column_sync_name": "comic_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 8, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": "title", + "table_id": "e86c5386-a155-4f40-a6f5-e8da635d39c4" + }, + { + "id": "9aa5d3a3-5af8-4fd4-a279-fef80d04ca8e", + "created_date": "2025-01-20 16:04:41.464000", + "last_modified_date": "2025-01-21 12:07:06.157000", + "version": 1, + "column_name": "name", + "column_sync_name": "name", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 7, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "a7a24f23-586b-4ac4-97ab-b610e2ab2e5d" + }, + { + "id": "9b494fb5-9d2f-43d2-91f2-7e6378fcd920", + "created_date": "2025-01-20 16:04:41.479000", + "last_modified_date": "2025-01-21 12:07:06.192000", + "version": 1, + "column_name": "rooster_id", + "column_sync_name": "rooster_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 8, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "727a04ab-67dd-4a80-b80c-d824e41bcda2" + }, + { + "id": "9cf2a827-633b-4506-889e-07be2c42c135", + "created_date": "2025-01-20 16:04:41.049000", + "last_modified_date": "2025-01-21 12:07:05.493000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 1, + "column_label": "ID", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" + }, + { + "id": "9d4b550e-f91e-4952-9803-f44055827ca3", + "created_date": "2025-01-20 16:04:41.168000", + "last_modified_date": "2025-01-21 12:07:05.692000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "13ea1a84-f92e-4ce6-b709-fe7c1893d229" + }, + { + "id": "9e2b8f30-e531-4911-9d4f-a66e4cae0104", + "created_date": "2025-01-20 16:04:41.184000", + "last_modified_date": "2025-01-21 12:07:05.719000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "ede5536c-ca16-48ec-bd0d-a3b00cc1fcab" + }, + { + "id": "9e674c4a-c227-471e-b088-ef146f7bbfe4", + "created_date": "2025-01-20 16:04:41.345000", + "last_modified_date": "2025-01-21 12:07:05.963000", + "version": 1, + "column_name": "book_id", + "column_sync_name": "book_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 6, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "e962f787-7795-4179-8168-b2992981da12" + }, + { + "id": "9e7e8cc9-b8f2-4078-9b4b-eb2241c333df", + "created_date": "2025-01-20 16:04:41.136000", + "last_modified_date": "2025-01-21 12:07:05.642000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "e86c5386-a155-4f40-a6f5-e8da635d39c4" + }, + { + "id": "9f5d1b59-658e-4cc8-8296-054f3f8d2b36", + "created_date": "2025-01-20 16:04:41.446000", + "last_modified_date": "2025-01-21 12:07:06.116000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "3ce78ef2-35c6-4397-8cdf-9241b52236df" + }, + { + "id": "9f62bb33-60dd-45ef-b911-9d5de31c9115", + "created_date": "2025-01-20 16:04:41.505000", + "last_modified_date": "2025-01-21 12:07:06.241000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "d8b65435-dacc-4129-b08a-f4588110e127" + }, + { + "id": "a078d8d6-61d9-4f5f-8f3c-7e3003772b31", + "created_date": "2025-01-20 16:04:41.630000", + "last_modified_date": "2025-01-21 12:07:06.472000", + "version": 1, + "column_name": "table_id", + "column_sync_name": "table_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 15, + "is_shown": 1, + "column_label": "Table", + "show_filter": 0, + "filter_label": null, + "ref_column": "table_name", + "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" + }, + { + "id": "a1006598-c0a4-49b6-b964-2c7d803149b6", + "created_date": "2025-01-20 16:04:41.336000", + "last_modified_date": "2025-01-21 12:07:05.951000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "e962f787-7795-4179-8168-b2992981da12" + }, + { + "id": "a2f3ea1d-2273-411a-bda9-e784a675987b", + "created_date": "2025-01-20 16:04:41.032000", + "last_modified_date": "2025-01-21 12:07:05.465000", + "version": 1, + "column_name": "title", + "column_sync_name": "title", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 8, + "is_shown": 1, + "column_label": "Title", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" + }, + { + "id": "a351720e-2bff-41a4-afd9-07a8f0409738", + "created_date": "2025-01-20 16:04:41.555000", + "last_modified_date": "2025-01-21 12:07:06.324000", + "version": 1, + "column_name": "module_name", + "column_sync_name": "module_name", + "column_type": "TEXT", + "column_modifier": "UNIQUE", + "column_order": 5, + "is_shown": 1, + "column_label": "Module", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "c92e35fc-b634-437a-bfa0-47d15c67fe83" + }, + { + "id": "a382ff07-84ac-4a1c-9fc1-4991b03fa117", + "created_date": "2025-01-20 16:04:41.566000", + "last_modified_date": "2025-01-21 12:07:06.340000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "d7584a1a-0249-45ed-85e9-532680643296" + }, + { + "id": "a507bbd8-c135-47f5-9294-4aee66335189", + "created_date": "2025-01-20 16:04:41.414000", + "last_modified_date": "2025-01-21 12:07:06.067000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "3773ac76-a957-42b2-b24d-5e66e4c32c12" + }, + { + "id": "a5358b3f-35cf-4268-b84d-6122adb4da84", + "created_date": "2025-01-20 16:04:41.017000", + "last_modified_date": "2025-01-21 12:07:05.432000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" + }, + { + "id": "a5711833-d162-4dd8-a2eb-694b3f70cae2", + "created_date": "2025-01-20 16:04:41.300000", + "last_modified_date": "2025-01-21 12:07:05.907000", + "version": 1, + "column_name": "author_id", + "column_sync_name": "author_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 6, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "7ebdc79a-d0a7-42dc-9e50-d93c79e182af" + }, + { + "id": "a756ca5d-b2a2-4d5e-8445-bac51633f340", + "created_date": "2025-01-20 16:04:41.551000", + "last_modified_date": "2025-01-21 12:07:06.314000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "c92e35fc-b634-437a-bfa0-47d15c67fe83" + }, + { + "id": "a7c70442-e850-4fc1-a3d6-330c2054f1d2", + "created_date": "2025-01-20 16:04:41.357000", + "last_modified_date": "2025-01-21 12:07:05.979000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "c96112de-b521-4e92-8021-0172a1aebaf8" + }, + { + "id": "a95be86c-310c-454a-ac15-0175f01bcc73", + "created_date": "2025-01-20 16:04:41.529000", + "last_modified_date": "2025-01-21 12:07:06.276000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "925989d3-faa9-4bb4-9183-3564b29063ec" + }, + { + "id": "a96e736e-4881-4a12-ac3f-4b24097cddde", + "created_date": "2025-01-20 16:04:41.619000", + "last_modified_date": "2025-01-21 12:07:06.449000", + "version": 1, + "column_name": "column_modifier", + "column_sync_name": "column_modifier", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 8, + "is_shown": 1, + "column_label": "Modifier", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" + }, + { + "id": "aaa29939-121e-4f0b-bacc-ac18c5c0288d", + "created_date": "2025-01-20 16:04:41.070000", + "last_modified_date": "2025-01-21 12:07:05.540000", + "version": 1, + "column_name": "path", + "column_sync_name": "path", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 10, + "is_shown": 1, + "column_label": "Verzeichnis", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" + }, + { + "id": "aff31eb7-2ec6-47ca-aab6-db72c7e44f35", + "created_date": "2025-01-20 16:04:41.399000", + "last_modified_date": "2025-01-21 12:07:06.041000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "a9170107-b28f-4476-947e-0aaa3a2a1e01" + }, + { + "id": "b096272e-a31e-41de-bd14-55f5179ee4f1", + "created_date": "2025-01-20 16:04:41.461000", + "last_modified_date": "2025-01-21 12:07:06.150000", + "version": 1, + "column_name": "insert_set", + "column_sync_name": "insert_set", + "column_type": "BOOLEAN", + "column_modifier": null, + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "a7a24f23-586b-4ac4-97ab-b610e2ab2e5d" + }, + { + "id": "b268026e-f505-4eb3-b729-dcf2a62dd481", + "created_date": "2025-01-20 16:04:41.146000", + "last_modified_date": "2025-01-21 12:07:05.656000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "e86c5386-a155-4f40-a6f5-e8da635d39c4" + }, + { + "id": "b2fd1e48-3b1b-4304-8861-f076f32fda54", + "created_date": "2025-01-20 16:04:41.038000", + "last_modified_date": "2025-01-21 12:07:05.478000", + "version": 1, + "column_name": "path", + "column_sync_name": "path", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 10, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" + }, + { + "id": "b4d20305-8b61-4a43-95a7-e6b1e0441951", + "created_date": "2025-01-20 16:04:41.418000", + "last_modified_date": "2025-01-21 12:07:06.073000", + "version": 1, + "column_name": "name", + "column_sync_name": "name", + "column_type": "TEXT", + "column_modifier": "UNIQUE", + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "3773ac76-a957-42b2-b24d-5e66e4c32c12" + }, + { + "id": "b5bc90fc-5c08-4861-9613-e76234ce00e4", + "created_date": "2025-01-20 16:04:41.514000", + "last_modified_date": "2025-01-21 12:07:06.256000", + "version": 1, + "column_name": "name", + "column_sync_name": "name", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 6, + "is_shown": 1, + "column_label": "Name", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "d8b65435-dacc-4129-b08a-f4588110e127" + }, + { + "id": "b6185d30-db67-418a-923e-52097c7575b3", + "created_date": "2025-01-20 16:04:41.411000", + "last_modified_date": "2025-01-21 12:07:06.059000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "3773ac76-a957-42b2-b24d-5e66e4c32c12" + }, + { + "id": "b6834a12-ba8a-48c0-b28b-7975a8844f0b", + "created_date": "2025-01-20 16:04:41.379000", + "last_modified_date": "2025-01-21 12:07:06.010000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "cf9b16b9-1ec0-4bed-ba95-fef65013c069" + }, + { + "id": "b7522c46-21eb-4811-a407-60fc413064af", + "created_date": "2025-01-20 16:04:41.295000", + "last_modified_date": "2025-01-21 12:07:05.899000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "7ebdc79a-d0a7-42dc-9e50-d93c79e182af" + }, + { + "id": "b97c0905-ab37-4ef8-9cc9-128c4352eb70", + "created_date": "2025-01-20 16:04:41.587000", + "last_modified_date": "2025-01-21 12:07:06.384000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "f99eb13a-b7c6-4f51-940e-2a2c36124a67" + }, + { + "id": "ba27d149-3c4e-4861-9482-426fa068be6c", + "created_date": "2025-01-20 16:04:41.148000", + "last_modified_date": "2025-01-21 12:07:05.660000", + "version": 1, + "column_name": "in_stock", + "column_sync_name": "in_stock", + "column_type": "BOOLEAN", + "column_modifier": null, + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "e86c5386-a155-4f40-a6f5-e8da635d39c4" + }, + { + "id": "bb720b08-b328-40bf-a93a-480f5d44dec8", + "created_date": "2025-01-20 16:04:41.397000", + "last_modified_date": "2025-01-21 12:07:06.038000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "a9170107-b28f-4476-947e-0aaa3a2a1e01" + }, + { + "id": "bb8ed0d2-da5f-4adf-9bdc-763af6b867b5", + "created_date": "2025-01-20 16:04:41.172000", + "last_modified_date": "2025-01-21 12:07:05.700000", + "version": 1, + "column_name": "name", + "column_sync_name": "name", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "13ea1a84-f92e-4ce6-b709-fe7c1893d229" + }, + { + "id": "bbeb1f07-ee1e-44b5-a6af-755e5090a64b", + "created_date": "2025-01-20 16:04:41.509000", + "last_modified_date": "2025-01-21 12:07:06.249000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "d8b65435-dacc-4129-b08a-f4588110e127" + }, + { + "id": "bc92dd14-8330-4c42-9a14-80edd8a84837", + "created_date": "2025-01-20 16:04:41.059000", + "last_modified_date": "2025-01-21 12:07:05.515000", + "version": 1, + "column_name": "url", + "column_sync_name": "link_url", + "column_type": "TEXT", + "column_modifier": "UNIQUE", + "column_order": 5, + "is_shown": 1, + "column_label": "URL", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" + }, + { + "id": "bd28b532-2354-4539-8041-fe4420f12066", + "created_date": "2025-01-20 16:04:41.570000", + "last_modified_date": "2025-01-21 12:07:06.349000", + "version": 1, + "column_name": "host", + "column_sync_name": "host", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 5, + "is_shown": 1, + "column_label": "Host", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "d7584a1a-0249-45ed-85e9-532680643296" + }, + { + "id": "bd3a6721-7d23-40e3-8e98-234e1f5edd8f", + "created_date": "2025-01-20 16:04:41.449000", + "last_modified_date": "2025-01-21 12:07:06.123000", + "version": 1, + "column_name": "player_id", + "column_sync_name": "player_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 6, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "3ce78ef2-35c6-4397-8cdf-9241b52236df" + }, + { + "id": "be352526-a240-4b0f-b9c1-87949d6a631e", + "created_date": "2025-01-20 16:04:41.193000", + "last_modified_date": "2025-01-21 12:07:05.736000", + "version": 1, + "column_name": "name", + "column_sync_name": "name", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 7, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "ede5536c-ca16-48ec-bd0d-a3b00cc1fcab" + }, + { + "id": "beee0fd2-ee99-4295-a66d-2b79abc0d266", + "created_date": "2025-01-20 16:04:41.248000", + "last_modified_date": "2025-01-21 12:07:05.820000", + "version": 1, + "column_name": "artist_id", + "column_sync_name": "artist_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "ae68f37b-9314-472c-86ea-387486f4db1c" + }, + { + "id": "befb9c7e-5075-4d76-9553-d2c91907ca5d", + "created_date": "2025-01-20 16:04:41.552000", + "last_modified_date": "2025-01-21 12:07:06.317000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "c92e35fc-b634-437a-bfa0-47d15c67fe83" + }, + { + "id": "c0d3f441-6227-446f-92f2-58d7c36da5e7", + "created_date": "2025-01-20 16:04:41.451000", + "last_modified_date": "2025-01-21 12:07:06.131000", + "version": 1, + "column_name": "team_id", + "column_sync_name": "team_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 8, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "3ce78ef2-35c6-4397-8cdf-9241b52236df" + }, + { + "id": "c11afaf1-5d18-4ba3-b9ff-bec4a5493af7", + "created_date": "2025-01-20 16:04:41.447000", + "last_modified_date": "2025-01-21 12:07:06.119000", + "version": 1, + "column_name": "year", + "column_sync_name": "year", + "column_type": "LONG", + "column_modifier": null, + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "3ce78ef2-35c6-4397-8cdf-9241b52236df" + }, + { + "id": "c13a35ae-b86f-4d79-a752-fb24c74b9f77", + "created_date": "2025-01-20 16:04:41.543000", + "last_modified_date": "2025-01-21 12:07:06.298000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "36c7ac9c-270d-40a4-af43-c952120f165c" + }, + { + "id": "c1989ee0-b0d0-4e01-983b-312276a1d328", + "created_date": "2025-01-20 16:04:41.276000", + "last_modified_date": "2025-01-21 12:07:05.868000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "721a49ce-a02d-4035-add9-126a4591cbb8" + }, + { + "id": "c2797d33-e8b9-4799-9025-0846b91f5527", + "created_date": "2025-01-20 16:04:41.629000", + "last_modified_date": "2025-01-21 12:07:06.468000", + "version": 1, + "column_name": "ref_column", + "column_sync_name": "ref_column", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 14, + "is_shown": 1, + "column_label": "Ref Column", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" + }, + { + "id": "c3058c53-38ee-4ea3-b9a4-7615e7a9448c", + "created_date": "2025-01-20 16:04:41.613000", + "last_modified_date": "2025-01-21 12:07:06.436000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" + }, + { + "id": "c37d106f-fe44-4e26-86e1-eeb433eb90e8", + "created_date": "2025-01-20 16:04:41.383000", + "last_modified_date": "2025-01-21 12:07:06.018000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "cf9b16b9-1ec0-4bed-ba95-fef65013c069" + }, + { + "id": "c6a6043e-f124-4762-913f-e25b81ef1572", + "created_date": "2025-01-20 16:04:41.493000", + "last_modified_date": "2025-01-21 12:07:06.216000", + "version": 1, + "column_name": "first_name", + "column_sync_name": "first_name", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 5, + "is_shown": 1, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c" + }, + { + "id": "c87d13f4-e745-4dfa-9a36-91c7715fa47a", + "created_date": "2025-01-20 16:04:41.263000", + "last_modified_date": "2025-01-21 12:07:05.848000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "1951f2b6-3640-4568-9c47-a20e0768a843" + }, + { + "id": "c8fb8955-7b61-4e52-b045-02035408fbd8", + "created_date": "2025-01-20 16:04:41.557000", + "last_modified_date": "2025-01-21 12:07:06.327000", + "version": 1, + "column_name": "import_data", + "column_sync_name": "import_data", + "column_type": "BOOLEAN", + "column_modifier": null, + "column_order": 6, + "is_shown": 1, + "column_label": "Import Data?", + "show_filter": 1, + "filter_label": "Import Data", + "ref_column": null, + "table_id": "c92e35fc-b634-437a-bfa0-47d15c67fe83" + }, + { + "id": "c9a7e0eb-412c-4bd1-a59a-05a0f6f14cdb", + "created_date": "2025-01-20 16:04:41.157000", + "last_modified_date": "2025-01-21 12:07:05.678000", + "version": 1, + "column_name": "volume_id", + "column_sync_name": "volume_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 9, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "e86c5386-a155-4f40-a6f5-e8da635d39c4" + }, + { + "id": "cb46dc70-6d43-4c3e-92b6-9686077a15aa", + "created_date": "2025-01-20 16:04:41.128000", + "last_modified_date": "2025-01-21 12:07:05.635000", + "version": 1, + "column_name": "publisher_id", + "column_sync_name": "publisher_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 8, + "is_shown": 1, + "column_label": "Verlag", + "show_filter": 0, + "filter_label": null, + "ref_column": "name", + "table_id": "26478de9-0bcc-427f-ba60-0a1ab79e107b" + }, + { + "id": "cc345232-345d-40ca-9440-81f6142ff0f9", + "created_date": "2025-01-20 16:04:41.571000", + "last_modified_date": "2025-01-21 12:07:06.353000", + "version": 1, + "column_name": "port", + "column_sync_name": "port", + "column_type": "LONG", + "column_modifier": null, + "column_order": 6, + "is_shown": 1, + "column_label": "Port", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "d7584a1a-0249-45ed-85e9-532680643296" + }, + { + "id": "ceac312b-fcc6-4fd1-a9d7-6ca8d86e9c02", + "created_date": "2025-01-20 16:04:41.210000", + "last_modified_date": "2025-01-21 12:07:05.762000", + "version": 1, + "column_name": "name", + "column_sync_name": "name", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "e4ab0e7b-4017-4ec0-9b33-b182a1850fda" + }, + { + "id": "cff3f14e-2af8-4267-9490-0b33d96da350", + "created_date": "2025-01-20 16:04:41.293000", + "last_modified_date": "2025-01-21 12:07:05.895000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "7ebdc79a-d0a7-42dc-9e50-d93c79e182af" + }, + { + "id": "d03c18ad-f7df-419c-8de9-a29540a607a6", + "created_date": "2025-01-20 16:04:41.014000", + "last_modified_date": "2025-01-21 12:07:05.426000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" + }, + { + "id": "d05c7280-9e5c-4afe-aa18-ce0b065731c3", + "created_date": "2025-01-20 16:04:41.612000", + "last_modified_date": "2025-01-21 12:07:06.432000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" + }, + { + "id": "d19e8269-31c6-4665-a6c8-aff4b2f687c2", + "created_date": "2025-01-20 16:04:41.494000", + "last_modified_date": "2025-01-21 12:07:06.219000", + "version": 1, + "column_name": "last_name", + "column_sync_name": "last_name", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 6, + "is_shown": 1, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c" + }, + { + "id": "d1dace0e-34fc-4b38-b5aa-0baf83a81f90", + "created_date": "2025-01-20 16:04:41.292000", + "last_modified_date": "2025-01-21 12:07:05.891000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "7ebdc79a-d0a7-42dc-9e50-d93c79e182af" + }, + { + "id": "d2220d74-e41c-4198-8ae1-535fd0d6cccb", + "created_date": "2025-01-20 16:04:41.621000", + "last_modified_date": "2025-01-21 12:07:06.452000", + "version": 1, + "column_name": "column_order", + "column_sync_name": "column_order", + "column_type": "LONG", + "column_modifier": null, + "column_order": 9, + "is_shown": 1, + "column_label": "Order", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" + }, + { + "id": "d2273139-9e75-44d2-b90b-a65281ab4d90", + "created_date": "2025-01-20 16:04:41.208000", + "last_modified_date": "2025-01-21 12:07:05.759000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "e4ab0e7b-4017-4ec0-9b33-b182a1850fda" + }, + { + "id": "d2388abd-b2fc-41ed-96af-93219db104c0", + "created_date": "2025-01-20 16:04:41.504000", + "last_modified_date": "2025-01-21 12:07:06.238000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 1, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "d8b65435-dacc-4129-b08a-f4588110e127" + }, + { + "id": "d2cbbac7-b7fa-4003-8fcd-106963b2f954", + "created_date": "2025-01-20 16:04:41.028000", + "last_modified_date": "2025-01-21 12:07:05.460000", + "version": 1, + "column_name": "should_download", + "column_sync_name": "should_download", + "column_type": "BOOLEAN", + "column_modifier": null, + "column_order": 7, + "is_shown": 1, + "column_label": "Download", + "show_filter": 1, + "filter_label": "Download", + "ref_column": null, + "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" + }, + { + "id": "d4d7a891-edbc-404e-b022-5f1635217e8f", + "created_date": "2025-01-20 16:04:41.588000", + "last_modified_date": "2025-01-21 12:07:06.388000", + "version": 1, + "column_name": "folder", + "column_sync_name": "folder", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 5, + "is_shown": 1, + "column_label": "Folder", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "f99eb13a-b7c6-4f51-940e-2a2c36124a67" + }, + { + "id": "d5d5a476-4497-49ae-8293-664f0b1ba8c2", + "created_date": "2025-01-20 16:04:41.119000", + "last_modified_date": "2025-01-21 12:07:05.621000", + "version": 1, + "column_name": "completed", + "column_sync_name": "completed", + "column_type": "BOOLEAN", + "column_modifier": null, + "column_order": 5, + "is_shown": 1, + "column_label": "Complete", + "show_filter": 1, + "filter_label": "Complete", + "ref_column": null, + "table_id": "26478de9-0bcc-427f-ba60-0a1ab79e107b" + }, + { + "id": "d5eff9c6-dfde-475a-949c-85374de3e846", + "created_date": "2025-01-20 16:04:41.359000", + "last_modified_date": "2025-01-21 12:07:05.983000", + "version": 1, + "column_name": "name", + "column_sync_name": "name", + "column_type": "TEXT", + "column_modifier": "UNIQUE", + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "c96112de-b521-4e92-8021-0172a1aebaf8" + }, + { + "id": "d6ab11fa-63ff-4ad2-9c49-c6b95c50de4a", + "created_date": "2025-01-20 16:04:41.370000", + "last_modified_date": "2025-01-21 12:07:06", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "1850a718-dd07-4766-9856-591d311a4d0d" + }, + { + "id": "d7f6b9ff-df50-42b6-90e9-c4dbf0f692eb", + "created_date": "2025-01-20 16:04:41.622000", + "last_modified_date": "2025-01-21 12:07:06.455000", + "version": 1, + "column_name": "is_shown", + "column_sync_name": "is_shown", + "column_type": "BOOLEAN", + "column_modifier": null, + "column_order": 10, + "is_shown": 1, + "column_label": "Is Shown", + "show_filter": 1, + "filter_label": "Is Shown", + "ref_column": null, + "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" + }, + { + "id": "dc6ddb0e-98ed-4c69-9072-2c8e96e04a55", + "created_date": "2025-01-20 16:04:41.290000", + "last_modified_date": "2025-01-21 12:07:05.886000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "7ebdc79a-d0a7-42dc-9e50-d93c79e182af" + }, + { + "id": "dd194c0d-330c-4e88-948c-20d7e0f806a3", + "created_date": "2025-01-20 16:04:41.195000", + "last_modified_date": "2025-01-21 12:07:05.740000", + "version": 1, + "column_name": "comic_id", + "column_sync_name": "comic_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 8, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": "title", + "table_id": "ede5536c-ca16-48ec-bd0d-a3b00cc1fcab" + }, + { + "id": "dd7392d7-dc9b-4d84-acac-a6ceebd20324", + "created_date": "2025-01-20 16:04:41.486000", + "last_modified_date": "2025-01-21 12:07:06.206000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c" + }, + { + "id": "dfdeabdf-5820-4d05-a790-353ad1eda8e4", + "created_date": "2025-01-20 16:04:41.212000", + "last_modified_date": "2025-01-21 12:07:05.766000", + "version": 1, + "column_name": "comic_id", + "column_sync_name": "comic_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 6, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "e4ab0e7b-4017-4ec0-9b33-b182a1850fda" + }, + { + "id": "e074a44e-79c7-464b-8959-1e971a27fe9f", + "created_date": "2025-01-20 16:04:41.259000", + "last_modified_date": "2025-01-21 12:07:05.839000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "1951f2b6-3640-4568-9c47-a20e0768a843" + }, + { + "id": "e1ca67f3-94d3-4459-91aa-f487087e83d6", + "created_date": "2025-01-20 16:04:41.063000", + "last_modified_date": "2025-01-21 12:07:05.526000", + "version": 1, + "column_name": "should_download", + "column_sync_name": "should_download", + "column_type": "BOOLEAN", + "column_modifier": null, + "column_order": 7, + "is_shown": 1, + "column_label": "Download", + "show_filter": 1, + "filter_label": "Download", + "ref_column": null, + "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" + }, + { + "id": "e2a22f91-7fb9-4261-a2a2-63a39141f5af", + "created_date": "2025-01-20 16:04:41.390000", + "last_modified_date": "2025-01-21 12:07:06.029000", + "version": 1, + "column_name": "last_name", + "column_sync_name": "last_name", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 6, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "cf9b16b9-1ec0-4bed-ba95-fef65013c069" + }, + { + "id": "e2d91173-a199-4e16-b190-4df3ed43e0f8", + "created_date": "2025-01-20 16:04:41.585000", + "last_modified_date": "2025-01-21 12:07:06.381000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "f99eb13a-b7c6-4f51-940e-2a2c36124a67" + }, + { + "id": "e2dd6cd2-188c-4423-a1f5-ab4c7ce421ab", + "created_date": "2025-01-20 16:04:41.513000", + "last_modified_date": "2025-01-21 12:07:06.253000", + "version": 1, + "column_name": "token", + "column_sync_name": "token", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 5, + "is_shown": 1, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "d8b65435-dacc-4129-b08a-f4588110e127" + }, + { + "id": "e45a35b2-9949-429c-967c-7ed3022ab115", + "created_date": "2025-01-20 16:04:41.450000", + "last_modified_date": "2025-01-21 12:07:06.127000", + "version": 1, + "column_name": "position_id", + "column_sync_name": "position_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 7, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "3ce78ef2-35c6-4397-8cdf-9241b52236df" + }, + { + "id": "e54bb4cb-748f-4b4a-b962-6b0b63d6cc59", + "created_date": "2025-01-20 16:04:41.087000", + "last_modified_date": "2025-01-21 12:07:05.573000", + "version": 1, + "column_name": "name", + "column_sync_name": "name", + "column_type": "TEXT", + "column_modifier": "UNIQUE", + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "c9060971-3f7d-4eb1-89e9-bb40d8687e77" + }, + { + "id": "e6d4e6e5-d859-4c6f-977a-43e77c5baff3", + "created_date": "2025-01-20 16:04:41.594000", + "last_modified_date": "2025-01-21 12:07:06.398000", + "version": 1, + "column_name": "sent_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 8, + "is_shown": 0, + "column_label": "Gesendet", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "f99eb13a-b7c6-4f51-940e-2a2c36124a67" + }, + { + "id": "e6e9e4cb-8a34-4ae6-aae3-961be9bd7ef1", + "created_date": "2025-01-20 16:04:41.544000", + "last_modified_date": "2025-01-21 12:07:06.302000", + "version": 1, + "column_name": "user_id", + "column_sync_name": "user_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 5, + "is_shown": 1, + "column_label": "User", + "show_filter": 0, + "filter_label": null, + "ref_column": "user_name", + "table_id": "36c7ac9c-270d-40a4-af43-c952120f165c" + }, + { + "id": "e7b24a7c-99f5-4101-937e-ffa1b9af115f", + "created_date": "2025-01-20 16:04:41.258000", + "last_modified_date": "2025-01-21 12:07:05.835000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "1951f2b6-3640-4568-9c47-a20e0768a843" + }, + { + "id": "e7cb1343-2a42-4a23-a894-5dd16e218afd", + "created_date": "2025-01-20 16:04:41.470000", + "last_modified_date": "2025-01-21 12:07:06.167000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "727a04ab-67dd-4a80-b80c-d824e41bcda2" + }, + { + "id": "e836258d-6315-40ee-97d3-33bec04d87fd", + "created_date": "2025-01-20 16:04:41.576000", + "last_modified_date": "2025-01-21 12:07:06.365000", + "version": 1, + "column_name": "password", + "column_sync_name": "password", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 9, + "is_shown": 0, + "column_label": "Password", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "d7584a1a-0249-45ed-85e9-532680643296" + }, + { + "id": "e8ee73c8-0b98-44a7-ac19-5b39a9eb0419", + "created_date": "2025-01-20 16:04:41.431000", + "last_modified_date": "2025-01-21 12:07:06.092000", + "version": 1, + "column_name": "name", + "column_sync_name": "name", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "9a1a6de3-eae1-48e7-88c4-0c67908ec1bf" + }, + { + "id": "ea50b9fc-6fab-4ee1-bb99-c64fa0ea781c", + "created_date": "2025-01-20 16:04:41.281000", + "last_modified_date": "2025-01-21 12:07:05.876000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "721a49ce-a02d-4035-add9-126a4591cbb8" + }, + { + "id": "eba35146-c5bf-41b8-b094-bb069413af8f", + "created_date": "2025-01-20 16:04:41.205000", + "last_modified_date": "2025-01-21 12:07:05.755000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "e4ab0e7b-4017-4ec0-9b33-b182a1850fda" + }, + { + "id": "eee39698-9f5c-4e89-a7d5-3b528eb9f185", + "created_date": "2025-01-20 16:04:41.562000", + "last_modified_date": "2025-01-21 12:07:06.333000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "d7584a1a-0249-45ed-85e9-532680643296" + }, + { + "id": "ef650efc-ba09-4ddd-882a-df0ff6ab745a", + "created_date": "2025-01-20 16:04:41.003000", + "last_modified_date": "2025-01-21 12:07:05.405000", + "version": 1, + "column_name": "review", + "column_sync_name": "review", + "column_type": "BOOLEAN", + "column_modifier": null, + "column_order": 6, + "is_shown": 1, + "column_label": "Review", + "show_filter": 1, + "filter_label": "Review", + "ref_column": null, + "table_id": "63285ce5-eb45-47f5-9575-dec1f9778bbc" + }, + { + "id": "f136c838-f004-4acf-8a61-38a99411c323", + "created_date": "2025-01-20 16:04:41.541000", + "last_modified_date": "2025-01-21 12:07:06.294000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "36c7ac9c-270d-40a4-af43-c952120f165c" + }, + { + "id": "f1836283-a6d3-4bfa-88f4-3eaf4181381e", + "created_date": "2025-01-20 16:04:41.241000", + "last_modified_date": "2025-01-21 12:07:05.807000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "ae68f37b-9314-472c-86ea-387486f4db1c" + }, + { + "id": "f1afc822-563f-4560-9e2e-6f9e18b40b00", + "created_date": "2025-01-20 16:04:41.531000", + "last_modified_date": "2025-01-21 12:07:06.279000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "925989d3-faa9-4bb4-9183-3564b29063ec" + }, + { + "id": "f30edb57-49e5-4b33-9063-b90ec68be198", + "created_date": "2025-01-20 16:04:41.054000", + "last_modified_date": "2025-01-21 12:07:05.504000", + "version": 1, + "column_name": "last_modified_date", + "column_sync_name": "modified", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 3, + "is_shown": 0, + "column_label": "Modified", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" + }, + { + "id": "f35111c8-09a3-45d6-aba7-ba7185cb2d41", + "created_date": "2025-01-20 16:04:41.100000", + "last_modified_date": "2025-01-21 12:07:05.591000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "33b748ee-9f55-487a-a614-2ab64b844a13" + }, + { + "id": "f614773e-52bf-4422-8ff7-5f1c6b4e46ab", + "created_date": "2025-01-20 16:04:41.572000", + "last_modified_date": "2025-01-21 12:07:06.357000", + "version": 1, + "column_name": "protocol", + "column_sync_name": "protocol", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 7, + "is_shown": 1, + "column_label": "Protocol", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "d7584a1a-0249-45ed-85e9-532680643296" + }, + { + "id": "f7bf5945-c316-4d93-a50d-636f64b6ecab", + "created_date": "2025-01-20 16:04:41.093000", + "last_modified_date": "2025-01-21 12:07:05.579000", + "version": 1, + "column_name": "id", + "column_sync_name": "identifier", + "column_type": "TEXT", + "column_modifier": "PRIMARY KEY", + "column_order": 1, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "33b748ee-9f55-487a-a614-2ab64b844a13" + }, + { + "id": "f82d7ffa-07ae-40d7-bd69-429dfcd28004", + "created_date": "2025-01-20 16:04:41.386000", + "last_modified_date": "2025-01-21 12:07:06.022000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "cf9b16b9-1ec0-4bed-ba95-fef65013c069" + }, + { + "id": "f83d93ca-9151-4d89-901e-d841a6ea5eba", + "created_date": "2025-01-20 16:04:41.111000", + "last_modified_date": "2025-01-21 12:07:05.606000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "26478de9-0bcc-427f-ba60-0a1ab79e107b" + }, + { + "id": "f8937b0c-73ee-4677-9011-e623b7415d0e", + "created_date": "2025-01-20 16:04:41.080000", + "last_modified_date": "2025-01-21 12:07:05.558000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "c9060971-3f7d-4eb1-89e9-bb40d8687e77" + }, + { + "id": "f9984d7f-b9c4-49e7-9277-7d8f295e21ae", + "created_date": "2025-01-20 16:04:41.035000", + "last_modified_date": "2025-01-21 12:07:05.472000", + "version": 1, + "column_name": "file_name", + "column_sync_name": "file_name", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 9, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" + }, + { + "id": "f9c63580-428d-4ccd-a02e-d40616dc9b48", + "created_date": "2025-01-20 16:04:41.265000", + "last_modified_date": "2025-01-21 12:07:05.852000", + "version": 1, + "column_name": "first_name", + "column_sync_name": "first_name", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 5, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "1951f2b6-3640-4568-9c47-a20e0768a843" + }, + { + "id": "fb661f6b-3693-4af8-8398-2e95278c538d", + "created_date": "2025-01-20 16:04:41.520000", + "last_modified_date": "2025-01-21 12:07:06.266000", + "version": 1, + "column_name": "user_id", + "column_sync_name": "user_id", + "column_type": "TEXT", + "column_modifier": null, + "column_order": 9, + "is_shown": 1, + "column_label": "User", + "show_filter": 0, + "filter_label": null, + "ref_column": "user_name", + "table_id": "d8b65435-dacc-4129-b08a-f4588110e127" + }, + { + "id": "fc565d02-e7bb-466e-9518-1a685b165419", + "created_date": "2025-01-20 16:04:41.181000", + "last_modified_date": "2025-01-21 12:07:05.714000", + "version": 1, + "column_name": "created_date", + "column_sync_name": "created", + "column_type": "TIMESTAMP", + "column_modifier": null, + "column_order": 2, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "ede5536c-ca16-48ec-bd0d-a3b00cc1fcab" + }, + { + "id": "fe55380f-938e-4b01-91a5-88a43795772f", + "created_date": "2025-01-20 16:04:41.416000", + "last_modified_date": "2025-01-21 12:07:06.070000", + "version": 1, + "column_name": "version", + "column_sync_name": "version", + "column_type": "LONG", + "column_modifier": null, + "column_order": 4, + "is_shown": 0, + "column_label": "", + "show_filter": 0, + "filter_label": null, + "ref_column": null, + "table_id": "3773ac76-a957-42b2-b24d-5e66e4c32c12" + } + ], + "comic": [ + { + "id": "0095a769-e5e2-441e-8a38-f98743ee9529", + "created_date": "2024-08-16 23:58:35.239000", + "last_modified_date": "2024-08-16 23:58:35.239000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Spider-Man: Breakout", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "010ffe6b-54e4-42d7-86b6-581e680c35ad", + "created_date": "2024-08-16 23:58:35.279000", + "last_modified_date": "2024-08-16 23:58:35.279000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "The Tenth", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "031b7570-04bc-4f56-834e-61c5792b6e5e", + "created_date": "2024-08-16 23:58:34.870000", + "last_modified_date": "2024-08-16 23:58:34.870000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Bart Simpson", + "publisher_id": "27b4304a-3bac-4ed3-9c2d-e8027d016dc0" + }, + { + "id": "03c5b145-69d4-4d7e-8323-cb2f81060829", + "created_date": "2024-08-16 23:58:34.903000", + "last_modified_date": "2024-08-16 23:58:34.903000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Danger Girl", + "publisher_id": "46cef536-96d3-442a-a871-fd133050053e" + }, + { + "id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", + "created_date": "2024-08-16 23:58:34.853000", + "last_modified_date": "2024-08-16 23:58:34.853000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Astonishing X-Men", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", + "created_date": "2024-08-16 23:58:35.161000", + "last_modified_date": "2024-08-16 23:58:35.161000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "New X-Men Academy X", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "07574acf-8f77-4da7-87fd-c49f40536baf", + "created_date": "2024-08-16 23:58:35.368000", + "last_modified_date": "2024-08-16 23:58:35.368000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Aria: The Uses of Enchantment", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "0cb7d5d0-4204-41a4-8698-b8dd56f1c597", + "created_date": "2024-08-16 23:58:35.308000", + "last_modified_date": "2024-08-16 23:58:35.308000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Vampirella", + "publisher_id": "c96d450d-5034-435b-9afe-c49c937b6328" + }, + { + "id": "0e6688d2-f347-4800-83b1-1f094b658084", + "created_date": "2024-08-16 23:58:35.198000", + "last_modified_date": "2024-08-16 23:58:35.198000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Shanna, The She-Devil", + "publisher_id": "978ef035-20af-4d89-b898-98159d8ce280" + }, + { + "id": "0f0bfd13-f6f0-436c-ad74-5e40ea6a0cf8", + "created_date": "2024-08-16 23:58:34.996000", + "last_modified_date": "2024-08-16 23:58:34.996000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Fathom: Killians Tide", + "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" + }, + { + "id": "0fd9969e-8e1f-4f42-94ac-fab4d2d614c2", + "created_date": "2024-08-16 23:58:34.849000", + "last_modified_date": "2024-08-16 23:58:34.849000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Aspen", + "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" + }, + { + "id": "100b82bf-c134-40d9-bcc2-a8b345030b8d", + "created_date": "2024-08-16 23:58:35.230000", + "last_modified_date": "2024-08-16 23:58:35.230000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Spider-Man India", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "11bc20d5-bfeb-4825-9e0f-3d6954020b07", + "created_date": "2024-08-16 23:58:34.943000", + "last_modified_date": "2024-08-16 23:58:34.943000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "El Cazador", + "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" + }, + { + "id": "12802b17-452a-4533-a7ab-fd94f19be123", + "created_date": "2024-08-16 23:58:34.977000", + "last_modified_date": "2024-08-16 23:58:34.977000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Fathom Dawn of War", + "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" + }, + { + "id": "12fde8f7-8ce3-4398-8095-5bb9b5d9508d", + "created_date": "2024-08-16 23:58:34.935000", + "last_modified_date": "2024-08-16 23:58:34.935000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Dragonlance: Chronicles", + "publisher_id": "1c3e0871-ca17-4766-935d-15e0ec33a5ac" + }, + { + "id": "13281ef7-5945-49a9-b8f1-c5e8548c18ec", + "created_date": "2024-08-16 23:58:35.331000", + "last_modified_date": "2024-08-16 23:58:35.331000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "X-23", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "134659ae-67a4-4cad-aac6-36bee893102f", + "created_date": "2024-08-16 23:58:35.354000", + "last_modified_date": "2024-08-16 23:58:35.354000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Runaways", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "14a359a2-c928-4e14-9fb2-249777e6a13a", + "created_date": "2024-08-16 23:58:35.364000", + "last_modified_date": "2024-08-16 23:58:35.364000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Aria: Summer\u00b4s Spell", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "17b50be7-aca7-446e-8729-3706f636d29d", + "created_date": "2024-08-16 23:58:35.113000", + "last_modified_date": "2024-08-16 23:58:35.113000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Mary Jane", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", + "created_date": "2024-08-16 23:58:34.826000", + "last_modified_date": "2024-08-16 23:58:34.826000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Amazing Fantasy", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "1b7de491-6bbb-404e-a2e5-a20a123e3fe5", + "created_date": "2024-08-16 23:58:35.135000", + "last_modified_date": "2024-08-16 23:58:35.135000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Mystic", + "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" + }, + { + "id": "1d9440ba-202a-4c4e-8425-8561606c1c3e", + "created_date": "2024-08-16 23:58:35.209000", + "last_modified_date": "2024-08-16 23:58:35.209000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Shrek", + "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" + }, + { + "id": "230efa0e-3aea-4b07-a05f-0f788f293d0b", + "created_date": "2024-08-16 23:58:35.320000", + "last_modified_date": "2024-08-16 23:58:35.320000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Witchblade / Tomb Raider", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "231e65eb-eecb-4946-a643-6184b767e321", + "created_date": "2024-08-16 23:58:34.840000", + "last_modified_date": "2024-08-16 23:58:34.840000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Aria", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "240a8a5d-eb07-4ce1-9fdd-a1b888c7426c", + "created_date": "2024-08-16 23:58:35.132000", + "last_modified_date": "2024-08-16 23:58:35.132000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Monster War 2005", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "25841b05-c246-4484-9ef2-71f8dcdb39ed", + "created_date": "2024-08-16 23:58:35.092000", + "last_modified_date": "2024-08-16 23:58:35.092000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Kiss Kiss Bang Bang", + "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" + }, + { + "id": "28adbced-8575-4858-8150-796857bb4129", + "created_date": "2024-08-16 23:58:35.336000", + "last_modified_date": "2024-08-16 23:58:35.336000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "X-Men: Age of Apocalypse One Shot", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "29dcafb5-2495-41d0-85df-6314e7bf8343", + "created_date": "2024-08-16 23:58:35.381000", + "last_modified_date": "2024-08-16 23:58:35.381000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Bomb Queen II: Queen of Hearts", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "2a4b287e-4b05-4016-8eb4-21fc225be24d", + "created_date": "2024-08-16 23:58:35.122000", + "last_modified_date": "2024-08-16 23:58:35.122000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Meridian", + "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" + }, + { + "id": "2ab84282-d7f5-43ef-af41-df0f756a62f7", + "created_date": "2024-08-16 23:58:35.376000", + "last_modified_date": "2024-08-16 23:58:35.376000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Army of Darkness: Shop Till You Drop Dead", + "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" + }, + { + "id": "2b2a07ba-c5a8-4cb8-b170-990cb941315a", + "created_date": "2024-08-16 23:58:35.218000", + "last_modified_date": "2024-08-16 23:58:35.218000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Soulfire", + "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" + }, + { + "id": "2c6ec683-8f23-4635-ab71-b377716213fc", + "created_date": "2024-08-16 23:58:35.396000", + "last_modified_date": "2024-08-16 23:58:35.396000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Star Wars: Knights of the Old Republic", + "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" + }, + { + "id": "2ca8751f-8d0a-449e-933f-f293e6fbd751", + "created_date": "2024-08-16 23:58:35.356000", + "last_modified_date": "2024-08-16 23:58:35.356000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Crux", + "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" + }, + { + "id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", + "created_date": "2024-08-16 23:58:35.267000", + "last_modified_date": "2024-08-16 23:58:35.267000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Tarot: Witch of the Black Rose", + "publisher_id": "587585ae-7002-4588-8716-f49d47ee05fc" + }, + { + "id": "3217d912-3b46-47d2-a6f8-a79c1eecfde1", + "created_date": "2024-08-16 23:58:35.277000", + "last_modified_date": "2024-08-16 23:58:35.277000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "The Devil\u00b4s Keeper", + "publisher_id": "479e4daf-f516-4eb1-af3a-ee9d4dcf5fee" + }, + { + "id": "33b14231-7f52-4a1d-8909-cb00e6a241ac", + "created_date": "2024-08-16 23:58:35.346000", + "last_modified_date": "2024-08-16 23:58:35.346000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "X-treme X-Men", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "34baf0ec-2af4-4ceb-9f54-ab92fb9a0b96", + "created_date": "2024-08-16 23:58:35.303000", + "last_modified_date": "2024-08-16 23:58:35.303000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Ultimate Spider-Man Annual", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "35894d94-1fb5-49de-ba77-cf2626cda939", + "created_date": "2024-08-16 23:58:35.143000", + "last_modified_date": "2024-08-16 23:58:35.143000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Necromancer", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "358d8ebf-8ada-4bc5-b47a-dd8a3b52b2c3", + "created_date": "2024-08-16 23:58:34.918000", + "last_modified_date": "2024-08-16 23:58:34.918000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Darkness / Tomb Raider", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "358f99c2-93f2-489d-83a4-03267fad8597", + "created_date": "2024-08-16 23:58:35.328000", + "last_modified_date": "2024-08-16 23:58:35.328000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Wraithborn", + "publisher_id": "38803fe4-26ef-40eb-8b2e-f718c6f27341" + }, + { + "id": "389378c4-d067-4f56-8842-6806b010b2d0", + "created_date": "2024-08-16 23:58:35.388000", + "last_modified_date": "2024-08-16 23:58:35.388000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Gen13", + "publisher_id": "38803fe4-26ef-40eb-8b2e-f718c6f27341" + }, + { + "id": "39536665-0c93-439f-97d4-be1f40da34dd", + "created_date": "2024-08-16 23:58:35.313000", + "last_modified_date": "2024-08-16 23:58:35.313000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Wildcats: Nemesis", + "publisher_id": "38803fe4-26ef-40eb-8b2e-f718c6f27341" + }, + { + "id": "39f1943d-0620-4d88-8709-c0bf8f35790d", + "created_date": "2024-08-16 23:58:35.006000", + "last_modified_date": "2024-08-16 23:58:35.006000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Freshmen", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "3e96be30-f58f-459d-bb97-cfa574b9487c", + "created_date": "2024-08-16 23:58:35.322000", + "last_modified_date": "2024-08-16 23:58:35.322000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Wolverine: The End", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "45bd0c8a-845f-4ab3-9281-c31e5a3d4472", + "created_date": "2024-08-16 23:58:35.236000", + "last_modified_date": "2024-08-16 23:58:35.236000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Spider-Man Team Up", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "45f57e1d-f3aa-40f7-b89a-b02eeda10577", + "created_date": "2024-08-16 23:58:35.325000", + "last_modified_date": "2024-08-16 23:58:35.325000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Wood Boy", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "46d02138-9fb9-4fbe-9928-d73c0faf2a4e", + "created_date": "2024-08-16 23:58:34.939000", + "last_modified_date": "2024-08-16 23:58:34.939000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Dream Police", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "46e41bf6-1c75-4e2b-9905-9dfda3bcfa9c", + "created_date": "2024-08-16 23:58:35.151000", + "last_modified_date": "2024-08-16 23:58:35.151000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "New Avengers", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "4a5c0ca1-9a08-49b9-bf7c-7bde4f35551a", + "created_date": "2024-08-16 23:58:34.879000", + "last_modified_date": "2024-08-16 23:58:34.879000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Birds of Prey", + "publisher_id": "c9efb32a-08ec-43bb-ba7c-95234146e96e" + }, + { + "id": "4b883248-716e-45b9-be2a-1eab276159bb", + "created_date": "2024-08-16 23:58:34.955000", + "last_modified_date": "2024-08-16 23:58:34.955000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Emma Frost", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "4f0e16a3-452f-43cd-9834-d0f4d2d5fca0", + "created_date": "2024-08-16 23:58:35.010000", + "last_modified_date": "2024-08-16 23:58:35.010000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Friendly Neighborhood Spider-Man", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "51bab5a9-915f-445c-8ab6-93de6120f88f", + "created_date": "2024-08-16 23:58:35.398000", + "last_modified_date": "2024-08-16 23:58:35.398000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Star Wars: Legacy", + "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" + }, + { + "id": "52f0849c-3c1c-49a0-9448-8b5b2c9b0c0c", + "created_date": "2024-08-16 23:58:35.270000", + "last_modified_date": "2024-08-16 23:58:35.270000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "The Art of Greg Horn", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "54ce47f2-d611-4d22-9ef5-c57e6d3e5967", + "created_date": "2024-08-16 23:58:34.900000", + "last_modified_date": "2024-08-16 23:58:34.900000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Crossgen", + "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" + }, + { + "id": "56152b4f-9a84-40ea-a329-8a267d931182", + "created_date": "2024-08-16 23:58:34.898000", + "last_modified_date": "2024-08-16 23:58:34.898000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Crimson", + "publisher_id": "38803fe4-26ef-40eb-8b2e-f718c6f27341" + }, + { + "id": "571cfe31-a696-41ca-8c28-ac68b17909ff", + "created_date": "2024-08-16 23:58:35.105000", + "last_modified_date": "2024-08-16 23:58:35.105000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Magdalena / Vampirella 2", + "publisher_id": "39b5fdd2-cfd1-4a9d-a8f3-6f011f6ce16a" + }, + { + "id": "57965b27-1330-4921-8c0b-4b09ee06084f", + "created_date": "2024-08-16 23:58:35.293000", + "last_modified_date": "2024-08-16 23:58:35.293000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Tomb Raider", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "5a72a63d-8fbd-46a8-b201-9b6e035c782a", + "created_date": "2024-08-16 23:58:34.861000", + "last_modified_date": "2024-08-16 23:58:34.861000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Athena Inc. The Manhunter Project", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "5aa143a9-d0a1-457f-b178-c8c71951dd91", + "created_date": "2024-08-16 23:58:35.301000", + "last_modified_date": "2024-08-16 23:58:35.301000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Ultimate Fantastic Four", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "5d0fd720-7875-4f9f-86eb-07ee0723908f", + "created_date": "2024-08-16 23:58:35.310000", + "last_modified_date": "2024-08-16 23:58:35.310000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Wild Girl", + "publisher_id": "38803fe4-26ef-40eb-8b2e-f718c6f27341" + }, + { + "id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", + "created_date": "2024-08-16 23:58:35.025000", + "last_modified_date": "2024-08-16 23:58:35.025000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Gift", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "5dbe4d8b-331a-41ad-bcdc-01196dc1d58d", + "created_date": "2024-08-16 23:58:35.343000", + "last_modified_date": "2024-08-16 23:58:35.343000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "X-Men: Phoenix - Endsong", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "5f648121-c503-46df-8a2b-56c11f5be6b4", + "created_date": "2024-08-16 23:58:34.959000", + "last_modified_date": "2024-08-16 23:58:34.959000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Excalibur", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "5fc6600f-b005-4d6b-a1af-a17cc2701d81", + "created_date": "2024-08-16 23:58:35.119000", + "last_modified_date": "2024-08-16 23:58:35.119000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Megacity 909", + "publisher_id": "1c3e0871-ca17-4766-935d-15e0ec33a5ac" + }, + { + "id": "60deca87-7b2a-4412-855f-01a6ccaeea56", + "created_date": "2024-08-16 23:58:35.340000", + "last_modified_date": "2024-08-16 23:58:35.340000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "X-Men: Kitty Pryde", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", + "created_date": "2024-08-16 23:58:34.892000", + "last_modified_date": "2024-08-16 23:58:34.892000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Brath", + "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" + }, + { + "id": "61a4d2fe-44b2-41bb-a19c-f7be95d5e195", + "created_date": "2024-08-16 23:58:34.967000", + "last_modified_date": "2024-08-16 23:58:34.967000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Fathom Beginnings", + "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" + }, + { + "id": "639eed1d-3ccf-4bfa-a595-06b44a4e5b8f", + "created_date": "2024-08-16 23:58:35.299000", + "last_modified_date": "2024-08-16 23:58:35.299000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Toxin", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "639f7e71-1012-49a6-bc3a-1ac7b1de3084", + "created_date": "2024-08-16 23:58:35.372000", + "last_modified_date": "2024-08-16 23:58:35.372000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Army of Darkness: Ashes 2 Ashes", + "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" + }, + { + "id": "63cfc38f-5f4e-4273-a630-7b455868687b", + "created_date": "2024-08-16 23:58:34.963000", + "last_modified_date": "2024-08-16 23:58:34.963000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Fathom", + "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" + }, + { + "id": "6d3e9abd-1c42-4024-903f-6b139571da25", + "created_date": "2024-08-16 23:58:35.049000", + "last_modified_date": "2024-08-16 23:58:35.049000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Harry Johnson", + "publisher_id": "c714d1b6-465f-4549-ba8d-0f2753863c4d" + }, + { + "id": "6f341583-4a3b-4d9a-b928-124f024fa005", + "created_date": "2024-08-16 23:58:35.188000", + "last_modified_date": "2024-08-16 23:58:35.188000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Rogue", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "6fd2c6fd-f8f1-4f13-892d-887b315fa4a3", + "created_date": "2024-08-16 23:58:34.929000", + "last_modified_date": "2024-08-16 23:58:34.929000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Darkness Vol. 2", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "6fff100e-050b-4da7-bdfa-10675dbab84e", + "created_date": "2024-08-16 23:58:35.282000", + "last_modified_date": "2024-08-16 23:58:35.282000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "The Tomb of Dracula", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "799309bc-d8d9-4d44-9457-bae7f1e7fcbf", + "created_date": "2024-08-16 23:58:35.351000", + "last_modified_date": "2024-08-16 23:58:35.351000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Army of Darkness vs. Re-Animator", + "publisher_id": "c96d450d-5034-435b-9afe-c49c937b6328" + }, + { + "id": "7c110b15-bbfc-472e-830b-b8db6ddb274e", + "created_date": "2024-08-16 23:58:35.333000", + "last_modified_date": "2024-08-16 23:58:35.333000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "X-Men: Age of Apocalypse", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "7e1bd46e-781b-4f6b-958b-0e9694cb7748", + "created_date": "2024-08-16 23:58:35.386000", + "last_modified_date": "2024-08-16 23:58:35.386000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Bomb Queen IV: Suicide Bomber", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "80944d0a-93fc-475b-bfb7-deed8c977832", + "created_date": "2024-08-16 23:58:35.392000", + "last_modified_date": "2024-08-16 23:58:35.392000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Iron & The Maiden", + "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" + }, + { + "id": "80f818f1-3813-4bf9-9eb2-0a441799fa6d", + "created_date": "2024-08-16 23:58:34.865000", + "last_modified_date": "2024-08-16 23:58:34.865000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Barbarossa & The Lost Corsairs", + "publisher_id": "9768502e-f0ab-446a-b095-2f363ff38c1c" + }, + { + "id": "82db30ac-0622-4785-9a43-95ae37e54eaa", + "created_date": "2024-08-16 23:58:35.085000", + "last_modified_date": "2024-08-16 23:58:35.085000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Iron Ghost", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "8441157d-4109-4dcd-a8a6-68b241d669fb", + "created_date": "2024-08-16 23:58:35.379000", + "last_modified_date": "2024-08-16 23:58:35.379000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "X-Men", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "851bc748-2602-4f20-826f-a59a7087d11f", + "created_date": "2024-08-16 23:58:34.823000", + "last_modified_date": "2024-08-16 23:58:34.823000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Abadazad", + "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" + }, + { + "id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", + "created_date": "2024-08-16 23:58:35.318000", + "last_modified_date": "2024-08-16 23:58:35.318000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Witchblade", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "8690631a-e99c-44be-887c-8fd2bb222ee1", + "created_date": "2024-08-16 23:58:35.102000", + "last_modified_date": "2024-08-16 23:58:35.102000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Lullaby", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "877c8105-ea6c-4624-b458-60d18b608c15", + "created_date": "2024-08-16 23:58:35.001000", + "last_modified_date": "2024-08-16 23:58:35.001000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Flak Riot", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "8955b2a3-84d3-4b55-a1f1-d45193600861", + "created_date": "2024-08-16 23:58:35.243000", + "last_modified_date": "2024-08-16 23:58:35.243000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Spider-Man: House of M", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "89c5ea13-997a-4831-87cc-ee76ea05c71e", + "created_date": "2024-08-16 23:58:34.885000", + "last_modified_date": "2024-08-16 23:58:34.885000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Black Widow 2", + "publisher_id": "978ef035-20af-4d89-b898-98159d8ce280" + }, + { + "id": "8a4558ac-33e9-4656-ab47-8292af313ff7", + "created_date": "2024-08-16 23:58:34.876000", + "last_modified_date": "2024-08-16 23:58:34.876000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Battle Pope", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "8a64d236-0f01-47cd-a841-6cc3fd6581ed", + "created_date": "2024-08-16 23:58:35.394000", + "last_modified_date": "2024-08-16 23:58:35.394000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Star Wars: Rebellion", + "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" + }, + { + "id": "8ad36a79-9436-455d-8096-0f1b73c22f13", + "created_date": "2024-08-16 23:58:35.195000", + "last_modified_date": "2024-08-16 23:58:35.195000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Scion", + "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" + }, + { + "id": "8e293af3-05c6-4dcc-9cbf-b87512ec975b", + "created_date": "2024-08-16 23:58:34.991000", + "last_modified_date": "2024-08-16 23:58:34.991000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Fathom Vol. 2", + "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" + }, + { + "id": "94837af3-bbaf-4496-b114-1676a3271cb6", + "created_date": "2024-08-16 23:58:34.813000", + "last_modified_date": "2024-08-16 23:58:34.813000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "1602", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "97685762-f1ae-4588-913f-0bdc38365360", + "created_date": "2024-08-16 23:58:34.974000", + "last_modified_date": "2024-08-16 23:58:34.974000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Fathom Cannon Hawke Prelude", + "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" + }, + { + "id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "created_date": "2024-08-16 23:58:35.139000", + "last_modified_date": "2024-08-16 23:58:35.139000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Mystique", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "9a175907-fea8-4f11-903f-6e837ce666c0", + "created_date": "2024-08-16 23:58:34.835000", + "last_modified_date": "2024-08-16 23:58:34.835000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Arana", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "9aac6484-4c06-4286-815a-219aad25cc74", + "created_date": "2024-08-16 23:58:35.058000", + "last_modified_date": "2024-08-16 23:58:35.058000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Hellcop", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "9b4d6ff0-f4f5-4bc6-8190-08755f35dbf1", + "created_date": "2024-08-16 23:58:34.888000", + "last_modified_date": "2024-08-16 23:58:34.888000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Bluntman and Chronic", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "9b924cdc-8959-41e0-a84d-f3e61bbeac44", + "created_date": "2024-08-16 23:58:35.021000", + "last_modified_date": "2024-08-16 23:58:35.021000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Ghostrider", + "publisher_id": "978ef035-20af-4d89-b898-98159d8ce280" + }, + { + "id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "created_date": "2024-08-16 23:58:35.214000", + "last_modified_date": "2024-08-16 23:58:35.214000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Sojourn", + "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" + }, + { + "id": "9fcdd9a5-f1fb-4421-a352-9da8e2c12f81", + "created_date": "2024-08-16 23:58:34.910000", + "last_modified_date": "2024-08-16 23:58:34.910000", + "version": 0, + "completed": 1, + "current_order": 0, + "title": "Daring Escapes", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "a08aef89-634c-494c-9def-73ff7e416464", + "created_date": "2024-08-16 23:58:35.067000", + "last_modified_date": "2024-08-16 23:58:35.067000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "House of M", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "created_date": "2024-08-16 23:58:35.224000", + "last_modified_date": "2024-08-16 23:58:35.224000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Spectacular Spider-Man", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "a2409ef1-82c3-45d1-9c65-b8806a31e525", + "created_date": "2024-08-16 23:58:35.182000", + "last_modified_date": "2024-08-16 23:58:35.182000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Red Sonja", + "publisher_id": "c96d450d-5034-435b-9afe-c49c937b6328" + }, + { + "id": "a557dbbc-2af1-4b56-8588-8fe42cf5c454", + "created_date": "2024-08-16 23:58:35.095000", + "last_modified_date": "2024-08-16 23:58:35.095000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Legend of Isis", + "publisher_id": "479e4daf-f516-4eb1-af3a-ee9d4dcf5fee" + }, + { + "id": "a5987e5c-0245-484d-aeb8-4b5195800d66", + "created_date": "2024-08-16 23:58:34.907000", + "last_modified_date": "2024-08-16 23:58:34.907000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Danger Girl Back in Black", + "publisher_id": "38803fe4-26ef-40eb-8b2e-f718c6f27341" + }, + { + "id": "ab909424-4ab4-4084-a47d-08ab865047e7", + "created_date": "2024-08-16 23:58:35.107000", + "last_modified_date": "2024-08-16 23:58:35.107000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Marvel Knights Spider-Man", + "publisher_id": "978ef035-20af-4d89-b898-98159d8ce280" + }, + { + "id": "ab9aa494-b791-498d-bf13-6c3c50c16667", + "created_date": "2024-08-16 23:58:35.291000", + "last_modified_date": "2024-08-16 23:58:35.291000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Tom Strong", + "publisher_id": "38803fe4-26ef-40eb-8b2e-f718c6f27341" + }, + { + "id": "abfcb11c-7757-4db8-879e-b0d1803819d9", + "created_date": "2024-08-16 23:58:35.154000", + "last_modified_date": "2024-08-16 23:58:35.154000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "New Mutants", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "af866a6a-2b51-499a-aa2d-b46743aabafd", + "created_date": "2024-08-16 23:58:35.250000", + "last_modified_date": "2024-08-16 23:58:35.250000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Stardust Kid", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", + "created_date": "2024-08-16 23:58:35.288000", + "last_modified_date": "2024-08-16 23:58:35.288000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Thor: Son of Asgard", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "b472b359-d586-458c-9042-a5fee057da3b", + "created_date": "2024-08-16 23:58:34.932000", + "last_modified_date": "2024-08-16 23:58:34.932000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "District X", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "b4c866cb-9461-4aa4-bc3b-ef3e63848775", + "created_date": "2024-08-16 23:58:35.360000", + "last_modified_date": "2024-08-16 23:58:35.360000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Aria: The Soul Market", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "b51c738d-8ba1-4451-8f89-f49c1968ac42", + "created_date": "2024-08-16 23:58:34.882000", + "last_modified_date": "2024-08-16 23:58:34.882000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Black Widow", + "publisher_id": "978ef035-20af-4d89-b898-98159d8ce280" + }, + { + "id": "b6383b7f-1c86-42d9-a3f6-d2a4bc96dc51", + "created_date": "2024-08-16 23:58:35.306000", + "last_modified_date": "2024-08-16 23:58:35.306000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Uncanny X-Men", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "b6713944-8d2d-4153-8f16-fe94cc4ee119", + "created_date": "2024-08-16 23:58:35.257000", + "last_modified_date": "2024-08-16 23:58:35.257000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Supergirl", + "publisher_id": "c9efb32a-08ec-43bb-ba7c-95234146e96e" + }, + { + "id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "created_date": "2024-08-16 23:58:35.014000", + "last_modified_date": "2024-08-16 23:58:35.014000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Futurama", + "publisher_id": "27b4304a-3bac-4ed3-9c2d-e8027d016dc0" + }, + { + "id": "b73e5e2f-60e4-42fe-94bf-44fe3755b8b8", + "created_date": "2024-08-16 23:58:35.193000", + "last_modified_date": "2024-08-16 23:58:35.193000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Samurai: Heaven & Earth", + "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" + }, + { + "id": "b9a8abf9-b259-4a6f-be2d-75f211788440", + "created_date": "2024-08-16 23:58:34.913000", + "last_modified_date": "2024-08-16 23:58:34.913000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Darkness / Superman", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "bba808d8-ede8-49fe-9ed5-3c23c7ca0f3c", + "created_date": "2024-08-16 23:58:34.980000", + "last_modified_date": "2024-08-16 23:58:34.980000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Fathom Prelude", + "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" + }, + { + "id": "be424be0-a91c-47b4-b4a9-1e1d760a7177", + "created_date": "2024-08-16 23:58:35.017000", + "last_modified_date": "2024-08-16 23:58:35.017000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Futurama Simpsons Crossover Crisis Part 2", + "publisher_id": "27b4304a-3bac-4ed3-9c2d-e8027d016dc0" + }, + { + "id": "becf411e-8fe2-470e-8bde-7991c59988e0", + "created_date": "2024-08-16 23:58:35.063000", + "last_modified_date": "2024-08-16 23:58:35.063000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Holiday Special 2004", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "bf23d317-f5b6-4cf8-8a05-4397888a82c9", + "created_date": "2024-08-16 23:58:34.895000", + "last_modified_date": "2024-08-16 23:58:34.895000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Catwoman When In Rome", + "publisher_id": "c9efb32a-08ec-43bb-ba7c-95234146e96e" + }, + { + "id": "bf59f643-455e-4b60-95b8-719d55437474", + "created_date": "2024-08-16 23:58:35.227000", + "last_modified_date": "2024-08-16 23:58:35.227000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Spellbinders", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "c03ae569-1a0b-4d17-8cfe-7e8972303751", + "created_date": "2024-08-16 23:58:35.383000", + "last_modified_date": "2024-08-16 23:58:35.383000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Bomb Queen III: The Good, The Bad and The Lovely", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "c0543c9f-d712-4dce-9b59-2bf73b800b31", + "created_date": "2024-08-16 23:58:35.110000", + "last_modified_date": "2024-08-16 23:58:35.110000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Marville", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "created_date": "2024-08-16 23:58:35.191000", + "last_modified_date": "2024-08-16 23:58:35.191000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Ruse", + "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" + }, + { + "id": "c0a044dd-461b-459a-9962-b94ed0e8b38f", + "created_date": "2024-08-16 23:58:34.986000", + "last_modified_date": "2024-08-16 23:58:34.986000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Fathom Swimsuit Special 2000", + "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" + }, + { + "id": "c686b9c2-abdc-4476-a530-ee85ce221a5d", + "created_date": "2024-08-16 23:58:35.233000", + "last_modified_date": "2024-08-16 23:58:35.233000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Spider-Man loves Mary Jane", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "c91ec109-0b4d-4fd6-995f-1a828958493f", + "created_date": "2024-08-16 23:58:35.254000", + "last_modified_date": "2024-08-16 23:58:35.254000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Strange", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "c9d4b26a-8431-4f68-8e7a-b61f2cf24176", + "created_date": "2024-08-16 23:58:35.164000", + "last_modified_date": "2024-08-16 23:58:35.164000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "New X-Men Hellions", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "cc96b25e-b827-4ff0-a94a-b82d30ca883c", + "created_date": "2024-08-16 23:58:35.157000", + "last_modified_date": "2024-08-16 23:58:35.157000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "New X-Men", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "ce879e17-f391-4de0-81c5-3279cbc87fc8", + "created_date": "2024-08-16 23:58:34.922000", + "last_modified_date": "2024-08-16 23:58:34.922000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Darkness / Vampirella", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "d16a57e0-5c70-4bdb-b71a-1b5290a838fd", + "created_date": "2024-08-16 23:58:35.401000", + "last_modified_date": "2024-08-16 23:58:35.401000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Star Wars: Dark Times", + "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" + }, + { + "id": "d23966db-c03d-4dd3-9f65-5aa18c689053", + "created_date": "2024-08-16 23:58:34.845000", + "last_modified_date": "2024-08-16 23:58:34.845000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Army of Darkness", + "publisher_id": "1c3e0871-ca17-4766-935d-15e0ec33a5ac" + }, + { + "id": "d24801e2-fbfe-4497-873f-4d8edb182ae4", + "created_date": "2024-08-16 23:58:35.261000", + "last_modified_date": "2024-08-16 23:58:35.261000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Superman", + "publisher_id": "c9efb32a-08ec-43bb-ba7c-95234146e96e" + }, + { + "id": "d33657a6-0ab7-4177-b5b3-4a0c9d358d03", + "created_date": "2024-08-16 23:58:35.147000", + "last_modified_date": "2024-08-16 23:58:35.147000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Negation War", + "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" + }, + { + "id": "d37740d1-9c0d-480f-bf14-16f868f50a2c", + "created_date": "2024-08-16 23:58:35.247000", + "last_modified_date": "2024-08-16 23:58:35.247000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Star Wars", + "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" + }, + { + "id": "d7dd6d02-bc9a-4fbf-a5ca-ad4728cb8109", + "created_date": "2024-08-16 23:58:34.873000", + "last_modified_date": "2024-08-16 23:58:34.873000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Bart Simpsons Treehouse of Horror", + "publisher_id": "27b4304a-3bac-4ed3-9c2d-e8027d016dc0" + }, + { + "id": "d99f789a-0d9c-4b12-bf1f-3b090fb0f1b8", + "created_date": "2024-08-16 23:58:34.948000", + "last_modified_date": "2024-08-16 23:58:34.948000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "El Cazador The Bloody Ballad of Blackjack Tom", + "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" + }, + { + "id": "db80cac0-8598-4063-9a5f-cd4f9c0f457c", + "created_date": "2024-08-16 23:58:34.926000", + "last_modified_date": "2024-08-16 23:58:34.926000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Darkness Black Sails", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "dc81f255-6757-4c41-8a7b-a06a503d7daa", + "created_date": "2024-08-16 23:58:34.983000", + "last_modified_date": "2024-08-16 23:58:34.983000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Fathom Swimsuit Special", + "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" + }, + { + "id": "de7ef7f5-daf8-4dfd-b8de-973c902a7df0", + "created_date": "2024-08-16 23:58:34.952000", + "last_modified_date": "2024-08-16 23:58:34.952000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Elsinore", + "publisher_id": "479e4daf-f516-4eb1-af3a-ee9d4dcf5fee" + }, + { + "id": "dea3ab08-5e3e-4438-b28f-fcb711a3b593", + "created_date": "2024-08-16 23:58:35.128000", + "last_modified_date": "2024-08-16 23:58:35.128000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Monster War", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "df47cdb1-0b41-4baa-83ce-fcfbb1c2bd51", + "created_date": "2024-08-16 23:58:35.071000", + "last_modified_date": "2024-08-16 23:58:35.071000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Hunter-Killer", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "e048cec2-52b9-48f8-9975-cfe17ed85aae", + "created_date": "2024-08-16 23:58:35.285000", + "last_modified_date": "2024-08-16 23:58:35.285000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Robert Jordan\u00b4s The Wheel of Time: New Spring", + "publisher_id": "ab7c10bf-8cd0-4a77-823f-a6764e9033d4" + }, + { + "id": "e1ff1410-ac3a-4ba5-8503-1fab529e50c0", + "created_date": "2024-08-16 23:58:35.186000", + "last_modified_date": "2024-08-16 23:58:35.186000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Revelations", + "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" + }, + { + "id": "e2e7a53a-fbd5-473c-9409-3acb87247728", + "created_date": "2024-08-16 23:58:35.169000", + "last_modified_date": "2024-08-16 23:58:35.169000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Nightcrawler", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "e4002ab9-0f26-48e6-9927-63c350df8015", + "created_date": "2024-08-16 23:58:34.858000", + "last_modified_date": "2024-08-16 23:58:34.858000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Athena Inc. The Beginning", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "e54ebebe-701a-4d60-88ce-4df9b34da6ca", + "created_date": "2024-08-16 23:58:35.200000", + "last_modified_date": "2024-08-16 23:58:35.200000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "She-Hulk", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "e6b93088-b350-412b-9634-227216ff252a", + "created_date": "2024-08-16 23:58:35.043000", + "last_modified_date": "2024-08-16 23:58:35.043000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Hack/Slash: Girls Gone Dead", + "publisher_id": "1c3e0871-ca17-4766-935d-15e0ec33a5ac" + }, + { + "id": "ea903b99-4032-4b4e-add4-177e051733a8", + "created_date": "2024-08-16 23:58:35.029000", + "last_modified_date": "2024-08-16 23:58:35.029000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Hack Slash Land of Lost Toys", + "publisher_id": "1c3e0871-ca17-4766-935d-15e0ec33a5ac" + }, + { + "id": "ed58b16e-0701-4373-befe-39118bc2d4cb", + "created_date": "2024-08-16 23:58:35.264000", + "last_modified_date": "2024-08-16 23:58:35.264000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Superman/Batman", + "publisher_id": "c9efb32a-08ec-43bb-ba7c-95234146e96e" + }, + { + "id": "ef71fb0f-ecba-4d88-ae5f-90c81b0c4e18", + "created_date": "2024-08-16 23:58:35.316000", + "last_modified_date": "2024-08-16 23:58:35.316000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Wildsiderz", + "publisher_id": "38803fe4-26ef-40eb-8b2e-f718c6f27341" + }, + { + "id": "efc52177-93a0-4e69-a76f-2c9049ff3967", + "created_date": "2024-08-16 23:58:35.178000", + "last_modified_date": "2024-08-16 23:58:35.178000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Radix", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", + "created_date": "2024-08-16 23:58:35.211000", + "last_modified_date": "2024-08-16 23:58:35.211000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Simpsons", + "publisher_id": "27b4304a-3bac-4ed3-9c2d-e8027d016dc0" + }, + { + "id": "f3231681-cd2b-4ff9-bfa2-5d8f631bee4d", + "created_date": "2024-08-16 23:58:35.117000", + "last_modified_date": "2024-08-16 23:58:35.117000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Mary Jane Homecoming", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "f49085fd-c407-4aa8-bc57-118dde083369", + "created_date": "2024-08-16 23:58:35.125000", + "last_modified_date": "2024-08-16 23:58:35.125000", + "version": 0, + "completed": 1, + "current_order": 0, + "title": "Midnight Nation", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "f4cb5b24-00ea-4249-9d09-45432c168b8f", + "created_date": "2024-08-16 23:58:35.221000", + "last_modified_date": "2024-08-16 23:58:35.221000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Soulfire Dying of the Light", + "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" + }, + { + "id": "f4f227f0-a6d1-49fd-baea-6791245a89e7", + "created_date": "2024-08-16 23:58:34.970000", + "last_modified_date": "2024-08-16 23:58:34.970000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Fathom Cannon Hawke", + "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" + }, + { + "id": "f6e4b186-dca5-46f6-8b4d-eb52b32265e5", + "created_date": "2024-08-16 23:58:34.818000", + "last_modified_date": "2024-08-16 23:58:34.818000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "10th Muse", + "publisher_id": "479e4daf-f516-4eb1-af3a-ee9d4dcf5fee" + }, + { + "id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "created_date": "2024-08-16 23:58:34.830000", + "last_modified_date": "2024-08-16 23:58:34.830000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Amazing Spider-Man", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "fa203e3e-db4a-44ed-beab-df526fec838c", + "created_date": "2024-08-16 23:58:35.082000", + "last_modified_date": "2024-08-16 23:58:35.082000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Hunter-Killer Dossier", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "fd313197-70f1-45d5-8ca3-c8b6d828254e", + "created_date": "2024-08-16 23:58:35.203000", + "last_modified_date": "2024-08-16 23:58:35.203000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "She-Hulk 2", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "fdcbf2b1-c3cb-44d8-888b-41260a87b0e4", + "created_date": "2024-08-16 23:58:35.296000", + "last_modified_date": "2024-08-16 23:58:35.296000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Tomb Raider: The Greatest Treasure of All", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + }, + { + "id": "fdccf7d0-db2c-4b01-a412-66e3ff043fe9", + "created_date": "2024-08-16 23:58:35.173000", + "last_modified_date": "2024-08-16 23:58:35.173000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Ororo: Before the Storm", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "fe69cf80-946a-45d6-9fba-55ebfc0f038c", + "created_date": "2024-08-16 23:58:35.098000", + "last_modified_date": "2024-08-16 23:58:35.098000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Loki", + "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" + }, + { + "id": "fe7fb34e-5ff2-4f6d-a649-0be19a368f46", + "created_date": "2024-08-16 23:58:35.207000", + "last_modified_date": "2024-08-16 23:58:35.207000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Shi Ju-Nen", + "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" + }, + { + "id": "ff81bcf6-b368-4264-872c-544e85ec80e8", + "created_date": "2024-08-16 23:58:35.216000", + "last_modified_date": "2024-08-16 23:58:35.216000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "Solus", + "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" + }, + { + "id": "fffaa1a6-5c3d-4deb-b759-35de74c65958", + "created_date": "2024-08-16 23:58:35.088000", + "last_modified_date": "2024-08-16 23:58:35.088000", + "version": 0, + "completed": 0, + "current_order": 0, + "title": "J.U.D.G.E.: Secret Rage", + "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" + } + ], + "media_video": [ + { + "id": "0aa9240c-3de9-4818-82d3-327f8103a36d", + "created_date": "2024-09-05 19:19:00.839024", + "last_modified_date": "2024-09-05 19:19:00.839024", + "version": 0, + "url": "https://www.youtube.com/watch?v=ReaFJ_MD2cs", + "review": 0, + "should_download": 1, + "title": "BLIND GUARDIAN - Secrets Of The American Gods (OFFICIAL MUSIC VIDEO) - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "2d8d3587-a382-42e2-8db2-8b2a83b1afa1", + "created_date": "2024-09-05 19:19:00.915634", + "last_modified_date": "2024-09-05 19:19:00.915634", + "version": 0, + "url": "https://www.youtube.com/watch?v=shDNKBMd6aw", + "review": 0, + "should_download": 1, + "title": "Nightwish & Floor Jansen - Song of Myself (Live @ Wacken 2013) - Lyric Video - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "3299aa51-0f99-4984-9f44-dcdde5098b5b", + "created_date": "2024-09-05 19:19:00.879147", + "last_modified_date": "2024-09-05 19:19:00.879147", + "version": 0, + "url": "https://www.youtube.com/watch?v=oSnuV_QmB-A", + "review": 0, + "should_download": 1, + "title": "Versengold - Die wilde Jagd (Offizielles Video) - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "33802d64-0711-4641-aeb3-842e06cc3aa7", + "created_date": "2024-09-05 19:19:00.943202", + "last_modified_date": "2024-09-05 19:19:00.943202", + "version": 0, + "url": "https://www.youtube.com/watch?v=oHCaZmIzr0o", + "review": 0, + "should_download": 1, + "title": "Nightwish - Perfume Of The Timeless (OFFICIAL MUSIC VIDEO) - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "3be2ab93-10b5-4c05-89c9-78ffa95725f2", + "created_date": "2024-09-05 19:19:00.926695", + "last_modified_date": "2024-09-05 19:19:00.926695", + "version": 0, + "url": "https://www.youtube.com/watch?v=gM0ol6y-qjQ", + "review": 0, + "should_download": 1, + "title": "Saltatio Mortis - Der Himmel muss warten (Official Lyric Video) - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "3f9dabea-fec1-40e2-bc62-2724d3cf816f", + "created_date": "2024-09-05 19:19:00.874536", + "last_modified_date": "2024-09-05 19:19:00.874536", + "version": 0, + "url": "https://www.youtube.com/watch?v=YWWp4uL53ho", + "review": 0, + "should_download": 1, + "title": "BLIND GUARDIAN - Ashes to Ashes (Live at Hellfest 2022) (OFFICIAL MUSIC VIDEO) - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "3fcbb868-8914-40df-b009-ff1361130c5d", + "created_date": "2024-09-05 19:19:00.890921", + "last_modified_date": "2024-09-05 19:19:00.890921", + "version": 0, + "url": "https://www.youtube.com/watch?v=J86zYZGApkc", + "review": 0, + "should_download": 1, + "title": "Die Toten Hosen // \u201a\u00c4\u00fbSteh auf, wenn du am Boden bist\u201a\u00c4\u00fa [Offizielles Musikvideo] - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "4062cb00-d436-4812-8fe2-892263edea35", + "created_date": "2024-09-05 19:19:00.900764", + "last_modified_date": "2024-09-05 19:19:00.900764", + "version": 0, + "url": "https://www.youtube.com/watch?v=zOvsyamoEDg", + "review": 0, + "should_download": 1, + "title": "FAUN - Federkleid (Offizielles Video) - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "4973e4a0-a9f9-4cd8-afa0-38699ee33498", + "created_date": "2024-09-05 19:19:00.824257", + "last_modified_date": "2024-09-05 19:19:00.824257", + "version": 0, + "url": "https://www.youtube.com/watch?v=zNHGJqgHvOU", + "review": 0, + "should_download": 1, + "title": "Blind Guardian - The Bard\u2018s Song - Live Wacken Open Air 2024 - 02.08.2024 - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "4e4510ca-70f8-441a-a61d-e62a1fb293d8", + "created_date": "2024-09-05 19:19:00.931969", + "last_modified_date": "2024-09-05 19:19:00.931969", + "version": 0, + "url": "https://www.youtube.com/watch?v=jWXsSwAMSHA", + "review": 0, + "should_download": 1, + "title": "Feuerschwanz - Schubsetanz - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "4e9ffca5-f7ea-460e-9e97-0985dd27cd42", + "created_date": "2024-09-16 13:59:16.510000", + "last_modified_date": "2024-09-16 13:59:16.510000", + "version": 0, + "url": "https://www.youtube.com/watch?v=yTypkLfDgMo", + "review": 0, + "should_download": 1, + "title": "FEUERSCHWANZ - Das Elfte Gebot (Official Video) | Napalm Records - YouTube", + "file_name": "", + "path": null, + "cloud_link": "" + }, + { + "id": "575801fa-40bb-472b-bc2d-6ba46e1424c9", + "created_date": "2024-09-05 19:19:00.971650", + "last_modified_date": "2024-09-05 19:19:00.971650", + "version": 0, + "url": "https://www.youtube.com/watch?v=5iz8dt3tknw", + "review": 0, + "should_download": 1, + "title": "Saltatio Mortis - Feuer und Erz (Official Video) - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "599d0001-86e9-49c3-901f-9240e1201078", + "created_date": "2024-09-05 19:19:00.911214", + "last_modified_date": "2024-09-05 20:12:13.227000", + "version": 1, + "url": "https://www.youtube.com/watch?v=VGko10RIGtY", + "review": 0, + "should_download": 1, + "title": "VERSENGOLD - Hoch die Kr\u00fcge (offizielles Video) | Zeitlos - YouTube", + "file_name": "", + "path": null, + "cloud_link": "" + }, + { + "id": "5a62e207-ebe9-4b5f-9a34-be34eaa533f1", + "created_date": "2024-09-05 19:19:00.895556", + "last_modified_date": "2024-09-05 19:19:00.895556", + "version": 0, + "url": "https://www.youtube.com/watch?v=sItCRENB1hU", + "review": 0, + "should_download": 1, + "title": "Faun ~ Wenn wir uns wiedersehen (Von Den Elben) - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "6c52cbea-a26f-4478-b77a-d7f71f7f3f54", + "created_date": "2024-09-05 19:19:00.960161", + "last_modified_date": "2024-09-05 19:19:00.960161", + "version": 0, + "url": "https://www.youtube.com/watch?v=09K4HJ31xj4", + "review": 0, + "should_download": 1, + "title": "Saltatio Mortis - Schwarzer Strand feat. Faun (Official Music Video) - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "84b32b93-5dde-4729-877c-6c8e1011d329", + "created_date": "2024-09-05 19:19:00.905726", + "last_modified_date": "2024-09-05 19:19:00.905726", + "version": 0, + "url": "https://www.youtube.com/watch?v=-J4AuEj4zHE", + "review": 0, + "should_download": 1, + "title": "Faun - Feuer (Offizielles Video) - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "856547c8-9863-47b9-85e3-9f3bfab3e0da", + "created_date": "2024-09-05 19:19:00.965710", + "last_modified_date": "2024-09-05 19:19:00.965710", + "version": 0, + "url": "https://www.youtube.com/watch?v=plMQ2r0z0K8", + "review": 0, + "should_download": 1, + "title": "Saltatio Mortis - Finsterwacht feat. Blind Guardian (Official Video) - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "88097439-8c05-4f19-ba5a-b81d1440bb01", + "created_date": "2024-09-05 19:19:00.954452", + "last_modified_date": "2024-09-05 19:19:00.954452", + "version": 0, + "url": "https://www.youtube.com/watch?v=pfrZE6JhJXU", + "review": 0, + "should_download": 1, + "title": "Schwarzer Strand | Saltatio Mortis X Faun | Making of - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "8ea48c73-02ea-43a3-9c9e-2800af71a132", + "created_date": "2024-09-16 13:59:36.878000", + "last_modified_date": "2024-09-16 13:59:36.878000", + "version": 0, + "url": "https://www.youtube.com/watch?v=WmlshlqXD54", + "review": 0, + "should_download": 1, + "title": "FEUERSCHWANZ ft. Melissa Bonny - Ding (SEEED Cover) | Napalm Records - YouTube", + "file_name": "", + "path": null, + "cloud_link": "" + }, + { + "id": "9378d488-4c4f-4fc5-acf9-25a44c4415b5", + "created_date": "2024-09-05 19:19:00.869366", + "last_modified_date": "2024-09-05 19:19:00.869366", + "version": 0, + "url": "https://www.youtube.com/watch?v=JyfE55c_ZjI", + "review": 0, + "should_download": 1, + "title": "BLIND GUARDIAN - Sacred Worlds (Sacred 2 - In-Game Concert) (OFFICIAL VIDEO) - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "958e1183-b901-446c-a596-0f41792486b4", + "created_date": "2024-09-05 19:19:00.885400", + "last_modified_date": "2024-09-05 19:19:00.885400", + "version": 0, + "url": "https://www.youtube.com/watch?v=nLgM1QJ3S_I", + "review": 0, + "should_download": 1, + "title": "FAUN - Walpurgisnacht (Official Video) - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "95eff153-4b89-46b7-a152-e482361293ac", + "created_date": "2024-09-05 19:19:00.832467", + "last_modified_date": "2024-09-05 19:19:00.832467", + "version": 0, + "url": "https://www.youtube.com/watch?v=fvtZ9_EN0xs", + "review": 0, + "should_download": 1, + "title": "BLIND GUARDIAN - The Quest for Tanelorn (Revisited) (OFFICIAL MUSIC VIDEO) - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "99822fe5-b1f6-4e9a-abd5-7a44d3a3867f", + "created_date": "2024-09-05 19:19:00.921273", + "last_modified_date": "2024-09-06 07:29:17.414000", + "version": 1, + "url": "https://www.ardmediathek.de/sendung/campervan-roadtrip/Y3JpZDovL2hyLW9ubGluZS8zODIyMDIxOQ", + "review": 0, + "should_download": 0, + "title": "Campervan-Roadtrip - alle verf\u00fcgbaren Videos - jetzt streamen!", + "file_name": "", + "path": null, + "cloud_link": "" + }, + { + "id": "a0c2c24d-7902-4f96-9d1e-ab9e48c93afe", + "created_date": "2024-09-05 19:19:00.864414", + "last_modified_date": "2024-09-05 19:19:00.864414", + "version": 0, + "url": "https://www.youtube.com/watch?v=SVg8eP7KPNQ", + "review": 0, + "should_download": 1, + "title": "BLIND GUARDIAN - Mirror Mirror (OFFICIAL LIVE VIDEO) - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "a8e549ac-aab7-4b39-891a-f594c581ab06", + "created_date": "2024-09-05 19:19:00.948913", + "last_modified_date": "2024-09-05 19:19:00.948913", + "version": 0, + "url": "https://www.youtube.com/watch?v=SOBzj0CZgiE", + "review": 0, + "should_download": 1, + "title": "Making of - Kaufmann und Maid | Behind the scenes | Saltatio Mortis - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "d6928639-8341-4d47-8af8-6ce86edebbd8", + "created_date": "2024-09-05 19:19:00.858012", + "last_modified_date": "2024-09-05 19:19:00.858012", + "version": 0, + "url": "https://www.youtube.com/watch?v=eIZNb96EQJ8", + "review": 0, + "should_download": 1, + "title": "BLIND GUARDIAN - A Voice In The Dark (OFFICIAL MUSIC VIDEO) - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + }, + { + "id": "f19e4f0d-e561-4313-b9d4-fec2277904fc", + "created_date": "2024-09-05 19:19:00.937716", + "last_modified_date": "2024-09-05 19:19:00.937716", + "version": 0, + "url": "https://www.youtube.com/watch?v=vaT0vAVBCMo", + "review": 0, + "should_download": 1, + "title": "FAUN - BLOT (Official Video) - YouTube", + "file_name": null, + "path": null, + "cloud_link": null + } + ], + "publisher": [ + { + "id": "083ae601-ad41-4d5f-b068-bf09e437675b", + "created_date": "2024-08-16 23:58:34.705000", + "last_modified_date": "2024-08-16 23:58:34.705000", + "version": 0, + "name": "Marvel" + }, + { + "id": "1c3e0871-ca17-4766-935d-15e0ec33a5ac", + "created_date": "2024-08-16 23:58:34.723000", + "last_modified_date": "2024-08-16 23:58:34.723000", + "version": 0, + "name": "Devils Due Publishing" + }, + { + "id": "27b4304a-3bac-4ed3-9c2d-e8027d016dc0", + "created_date": "2024-08-16 23:58:34.729000", + "last_modified_date": "2024-08-16 23:58:34.729000", + "version": 0, + "name": "Bongo Comics" + }, + { + "id": "38803fe4-26ef-40eb-8b2e-f718c6f27341", + "created_date": "2024-08-16 23:58:34.745000", + "last_modified_date": "2024-08-16 23:58:34.745000", + "version": 0, + "name": "WildStorm" + }, + { + "id": "39b5fdd2-cfd1-4a9d-a8f3-6f011f6ce16a", + "created_date": "2024-08-16 23:58:34.772000", + "last_modified_date": "2024-08-16 23:58:34.772000", + "version": 0, + "name": "Top Cow Productions" + }, + { + "id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306", + "created_date": "2024-08-16 23:58:34.715000", + "last_modified_date": "2024-08-16 23:58:34.715000", + "version": 0, + "name": "Crossgen" + }, + { + "id": "46cef536-96d3-442a-a871-fd133050053e", + "created_date": "2024-08-16 23:58:34.750000", + "last_modified_date": "2024-08-16 23:58:34.750000", + "version": 0, + "name": "Cliffhanger" + }, + { + "id": "479e4daf-f516-4eb1-af3a-ee9d4dcf5fee", + "created_date": "2024-08-16 23:58:34.711000", + "last_modified_date": "2024-08-16 23:58:34.711000", + "version": 0, + "name": "Alias" + }, + { + "id": "587585ae-7002-4588-8716-f49d47ee05fc", + "created_date": "2024-08-16 23:58:34.760000", + "last_modified_date": "2024-08-16 23:58:34.760000", + "version": 0, + "name": "Broadsword" + }, + { + "id": "61f5f1e5-dc60-4d24-be2d-352baa449c65", + "created_date": "2024-08-16 23:58:34.726000", + "last_modified_date": "2024-08-16 23:58:34.726000", + "version": 0, + "name": "Aspen" + }, + { + "id": "65330a74-7985-4f53-9316-27abc48259d5", + "created_date": "2024-08-16 23:58:34.754000", + "last_modified_date": "2024-08-16 23:58:34.754000", + "version": 0, + "name": "Dark Horse Comics" + }, + { + "id": "9768502e-f0ab-446a-b095-2f363ff38c1c", + "created_date": "2024-08-16 23:58:34.733000", + "last_modified_date": "2024-08-16 23:58:34.733000", + "version": 0, + "name": "Kandora" + }, + { + "id": "978ef035-20af-4d89-b898-98159d8ce280", + "created_date": "2024-08-16 23:58:34.741000", + "last_modified_date": "2024-08-16 23:58:34.741000", + "version": 0, + "name": "Marvel Knights" + }, + { + "id": "ab7c10bf-8cd0-4a77-823f-a6764e9033d4", + "created_date": "2024-08-16 23:58:34.768000", + "last_modified_date": "2024-08-16 23:58:34.768000", + "version": 0, + "name": "Red Eagle Entertainment" + }, + { + "id": "c714d1b6-465f-4549-ba8d-0f2753863c4d", + "created_date": "2024-08-16 23:58:34.775000", + "last_modified_date": "2024-08-16 23:58:34.775000", + "version": 0, + "name": "Pulp Fiction" + }, + { + "id": "c96d450d-5034-435b-9afe-c49c937b6328", + "created_date": "2024-08-16 23:58:34.764000", + "last_modified_date": "2024-08-16 23:58:34.764000", + "version": 0, + "name": "Dynamite Entertainment" + }, + { + "id": "c9efb32a-08ec-43bb-ba7c-95234146e96e", + "created_date": "2024-08-16 23:58:34.737000", + "last_modified_date": "2024-08-16 23:58:34.737000", + "version": 0, + "name": "DC" + }, + { + "id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b", + "created_date": "2024-08-16 23:58:34.718000", + "last_modified_date": "2024-08-16 23:58:34.718000", + "version": 0, + "name": "Image" + } + ], + "authorization_matrix": [ + { + "id": "02e76146-7741-4ad0-bb18-1f1697f4637c", + "created_date": "2024-08-16 23:58:34.662000", + "last_modified_date": "2024-08-16 23:58:34.662000", + "version": 0, + "user_id": "76b0e1f2-3ee7-44ae-80af-2fe6ff7f0b73", + "role_id": "dd6bad92-8ff0-4928-93ab-b356c75f2a81" + }, + { + "id": "059ab35e-c204-4c5a-acc3-4119662777ae", + "created_date": "2024-08-16 23:59:54.213000", + "last_modified_date": "2024-08-16 23:59:54.213000", + "version": 0, + "user_id": "3f60db8b-f6e4-47b6-9cde-d9f0040d981d", + "role_id": "0e0a5291-e74b-4ef8-823a-103f731e58bf" + }, + { + "id": "48a3422f-ee66-4e97-9404-1a4c130b9651", + "created_date": "2024-08-16 23:59:54.209000", + "last_modified_date": "2024-08-16 23:59:54.209000", + "version": 0, + "user_id": "3f60db8b-f6e4-47b6-9cde-d9f0040d981d", + "role_id": "d975bc73-1c21-4344-a500-4a90440edf7d" + }, + { + "id": "8a66287a-8798-4031-9cb2-dfefe0784aaa", + "created_date": "2024-08-16 23:58:34.679000", + "last_modified_date": "2024-08-16 23:58:34.679000", + "version": 0, + "user_id": "76b0e1f2-3ee7-44ae-80af-2fe6ff7f0b73", + "role_id": "0e0a5291-e74b-4ef8-823a-103f731e58bf" + }, + { + "id": "af3bcd8b-f680-4cd0-b0f0-d9fcd11cc4f5", + "created_date": "2024-08-16 23:59:54.216000", + "last_modified_date": "2024-08-16 23:59:54.216000", + "version": 0, + "user_id": "3f60db8b-f6e4-47b6-9cde-d9f0040d981d", + "role_id": "dd6bad92-8ff0-4928-93ab-b356c75f2a81" + }, + { + "id": "d2b19e65-a129-426e-b7e3-c9bf2f0a6542", + "created_date": "2024-10-23 17:23:46.625000", + "last_modified_date": "2024-10-23 17:23:46.625000", + "version": 0, + "user_id": "3f60db8b-f6e4-47b6-9cde-d9f0040d981d", + "role_id": "05a186f6-36a2-4cce-8904-187301193937" + }, + { + "id": "e408ff0f-33eb-4151-bb75-482452fc6b35", + "created_date": "2024-08-16 23:59:58.569000", + "last_modified_date": "2024-08-16 23:59:58.569000", + "version": 0, + "user_id": "76b0e1f2-3ee7-44ae-80af-2fe6ff7f0b73", + "role_id": "d975bc73-1c21-4344-a500-4a90440edf7d" + } + ], + "vendor": [ + { + "id": "035fb6c7-c702-400f-97bd-b98ad12963bb", + "created_date": "2024-08-16 23:58:37.457000", + "last_modified_date": "2024-08-16 23:58:37.457000", + "version": 0, + "name": "Donruss" + }, + { + "id": "18f1263f-ef9a-42d0-9391-1ead13b16880", + "created_date": "2024-08-16 23:58:37.456000", + "last_modified_date": "2024-08-16 23:58:37.456000", + "version": 0, + "name": "Topps" + }, + { + "id": "220d410d-75a2-4b33-94cf-b9fca23666eb", + "created_date": "2024-08-16 23:58:37.460000", + "last_modified_date": "2024-08-16 23:58:37.460000", + "version": 0, + "name": "Flair" + }, + { + "id": "5d409413-db78-43b3-bc7a-a76f5718c433", + "created_date": "2024-08-16 23:58:37.445000", + "last_modified_date": "2024-08-16 23:58:37.445000", + "version": 0, + "name": "Pacific" + }, + { + "id": "5e0f3f5d-c58b-4517-b30b-e767c34404ab", + "created_date": "2024-08-16 23:58:37.452000", + "last_modified_date": "2024-08-16 23:58:37.452000", + "version": 0, + "name": "Leaf" + }, + { + "id": "6472c0a1-2b4f-440f-813d-66ea3540f78c", + "created_date": "2024-08-16 23:58:37.454000", + "last_modified_date": "2024-08-16 23:58:37.454000", + "version": 0, + "name": "Upper Deck" + }, + { + "id": "d7617bf3-056a-489b-8563-628f2dd3da84", + "created_date": "2024-08-16 23:58:37.448000", + "last_modified_date": "2024-08-16 23:58:37.448000", + "version": 0, + "name": "Fleer" + }, + { + "id": "d9d8f727-7375-490a-8679-26c834b890a0", + "created_date": "2024-08-16 23:58:37.459000", + "last_modified_date": "2024-08-16 23:58:37.459000", + "version": 0, + "name": "Score" + }, + { + "id": "eb42fc0c-20ad-4e5b-b99a-552ac861db05", + "created_date": "2024-08-16 23:58:37.450000", + "last_modified_date": "2024-08-16 23:58:37.450000", + "version": 0, + "name": "Bowman" + } + ], + "rooster": [ + { + "id": "0ff455c2-8962-4d9d-aedd-2909b251e17d", + "created_date": "2024-08-16 23:58:37.569000", + "last_modified_date": "2024-08-16 23:58:37.569000", + "version": 0, + "year": 2001, + "player_id": "02fc65f6-7d12-41c1-936d-e5dd20da58ba", + "position_id": "d113f005-7792-494d-ad98-b1ce9a30130c", + "team_id": "3fec6af3-d87d-4dd5-b15c-1c6982135920" + }, + { + "id": "3f64b27d-893a-4c70-997e-a5d820a9aa45", + "created_date": "2024-08-16 23:58:37.549000", + "last_modified_date": "2024-08-16 23:58:37.549000", + "version": 0, + "year": 2001, + "player_id": "d6b1a09d-3984-41cb-8b0a-d37d77e2c8ff", + "position_id": "3682a19c-52de-40d5-9307-126d4c579d69", + "team_id": "88fb6005-0ec2-4e5e-8f78-8c2c172c0ce5" + }, + { + "id": "437bab97-1abf-4b41-b118-3c607dfd635d", + "created_date": "2024-08-16 23:58:37.559000", + "last_modified_date": "2024-08-16 23:58:37.559000", + "version": 0, + "year": 2001, + "player_id": "887504f8-5038-4335-90ba-487ac139155c", + "position_id": "0a2976b2-a6f5-4e87-86e0-97270f64958d", + "team_id": "0c1d90c1-a0cb-4f58-94e6-58a419e4d8b8" + }, + { + "id": "593e70f9-fcea-428d-8dbd-a4d4909e54ad", + "created_date": "2024-08-16 23:58:37.554000", + "last_modified_date": "2024-08-16 23:58:37.554000", + "version": 0, + "year": 2001, + "player_id": "a189b76f-9390-49f1-b5ab-47b35d00cb7f", + "position_id": "cd7e15d3-2d95-415b-9dfe-18ca3ed53a26", + "team_id": "934264a3-0333-44f3-a64a-8cb4b9a8e81e" + }, + { + "id": "8842ec4c-fddd-46ea-9709-a35c78441b07", + "created_date": "2024-08-16 23:58:37.565000", + "last_modified_date": "2024-08-16 23:58:37.565000", + "version": 0, + "year": 2001, + "player_id": "e83cddec-19b0-4d35-929c-8acd7a7b7cbf", + "position_id": "e4201d7e-fe49-439e-ad2e-e95f27f05ed5", + "team_id": "1cdf0d19-2ed8-471c-b689-cca82c557a5d" + }, + { + "id": "ad4d73ec-97c2-4f6e-8e31-fff4e38985de", + "created_date": "2024-08-16 23:58:37.562000", + "last_modified_date": "2024-08-16 23:58:37.562000", + "version": 0, + "year": 2001, + "player_id": "5ce64865-db01-4c55-9bb1-72ea1bea9b08", + "position_id": "3682a19c-52de-40d5-9307-126d4c579d69", + "team_id": "1cdf0d19-2ed8-471c-b689-cca82c557a5d" + }, + { + "id": "b773e310-61dd-4a9c-8478-e80d1db1faaf", + "created_date": "2024-08-16 23:58:37.557000", + "last_modified_date": "2024-08-16 23:58:37.557000", + "version": 0, + "year": 2001, + "player_id": "5ee26469-7618-45ef-ba98-f83c3931f2d3", + "position_id": "e4201d7e-fe49-439e-ad2e-e95f27f05ed5", + "team_id": "0c1d90c1-a0cb-4f58-94e6-58a419e4d8b8" + }, + { + "id": "c294649f-f36b-4ba2-a695-6329820332fa", + "created_date": "2024-08-16 23:58:37.571000", + "last_modified_date": "2024-08-16 23:58:37.571000", + "version": 0, + "year": 2001, + "player_id": "983cc139-f2a2-46cd-9fdc-3ec1abf0d5a5", + "position_id": "e4201d7e-fe49-439e-ad2e-e95f27f05ed5", + "team_id": "3fec6af3-d87d-4dd5-b15c-1c6982135920" + }, + { + "id": "cba79538-b728-42b4-9e85-91fc3609f194", + "created_date": "2024-08-16 23:58:37.566000", + "last_modified_date": "2024-08-16 23:58:37.566000", + "version": 0, + "year": 2002, + "player_id": "e83cddec-19b0-4d35-929c-8acd7a7b7cbf", + "position_id": "e4201d7e-fe49-439e-ad2e-e95f27f05ed5", + "team_id": "179f15e0-6628-40a0-b79d-30fffa5445b2" + }, + { + "id": "da266d28-01c9-4f5f-a166-d984743fa9a5", + "created_date": "2024-08-16 23:58:37.560000", + "last_modified_date": "2024-08-16 23:58:37.560000", + "version": 0, + "year": 2001, + "player_id": "5038dd4f-203f-4cb7-99dd-c12c7da4735f", + "position_id": "d113f005-7792-494d-ad98-b1ce9a30130c", + "team_id": "1cdf0d19-2ed8-471c-b689-cca82c557a5d" + }, + { + "id": "f7d50eb8-b42f-4240-8bd6-2321f0d94d93", + "created_date": "2024-08-16 23:58:37.568000", + "last_modified_date": "2024-08-16 23:58:37.568000", + "version": 0, + "year": 2001, + "player_id": "cd4e3f5f-1e03-4d9b-973a-e8f9b7892dd8", + "position_id": "54aedda1-1732-4f14-98f4-0aecc924df15", + "team_id": "3fec6af3-d87d-4dd5-b15c-1c6982135920" + } + ], + "media_article": [], + "user": [ + { + "id": "3f60db8b-f6e4-47b6-9cde-d9f0040d981d", + "created_date": "2024-08-16 23:59:54.185000", + "last_modified_date": "2024-08-16 23:59:54.185000", + "version": 0, + "first_name": "Thomas", + "last_name": "Peetz", + "user_name": "tpeetz", + "email": "thomas.peetz@thpeetz.de", + "password": "$2a$11$6QaMld8u3ONEKuCKLDCzqua4/CL8MBgskq4TaKb/BW2tFs8VsOy6S", + "enabled": 1 + }, + { + "id": "76b0e1f2-3ee7-44ae-80af-2fe6ff7f0b73", + "created_date": "2024-08-16 23:58:34.642000", + "last_modified_date": "2024-08-16 23:58:34.642000", + "version": 0, + "first_name": "Admin", + "last_name": "Administrator", + "user_name": "admin", + "email": "admin@example.org", + "password": "$2a$11$ghUnLGowsDn8wr67fGTJ4OxdEI3H7yL8Do5cR4/E.jhNmHg7TO8Jy", + "enabled": 0 + } + ], + "article": [], + "card": [ + { + "id": "0bf12b65-9739-4418-a792-47f4a97077d5", + "created_date": "2024-08-16 23:58:37.594000", + "last_modified_date": "2024-08-16 23:58:37.594000", + "version": 0, + "card_number": 103, + "year": 2001, + "card_set_id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", + "rooster_id": "b773e310-61dd-4a9c-8478-e80d1db1faaf", + "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" + }, + { + "id": "18935b6e-744b-4297-9840-9a4a71543997", + "created_date": "2024-08-16 23:58:37.596000", + "last_modified_date": "2024-08-16 23:58:37.596000", + "version": 0, + "card_number": 112, + "year": 2001, + "card_set_id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", + "rooster_id": "437bab97-1abf-4b41-b118-3c607dfd635d", + "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" + }, + { + "id": "239e1b5c-38f9-41e1-9798-fe365e878850", + "created_date": "2024-08-16 23:58:37.604000", + "last_modified_date": "2024-08-16 23:58:37.604000", + "version": 0, + "card_number": 335, + "year": 2001, + "card_set_id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", + "rooster_id": "0ff455c2-8962-4d9d-aedd-2909b251e17d", + "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" + }, + { + "id": "47432779-9460-4c09-9823-8520feb3b63b", + "created_date": "2024-08-16 23:58:37.588000", + "last_modified_date": "2024-08-16 23:58:37.588000", + "version": 0, + "card_number": 185, + "year": 2001, + "card_set_id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", + "rooster_id": "3f64b27d-893a-4c70-997e-a5d820a9aa45", + "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" + }, + { + "id": "8bcf6a19-596f-4be3-bfbe-0642e78122b5", + "created_date": "2024-08-16 23:58:37.603000", + "last_modified_date": "2024-08-16 23:58:37.603000", + "version": 0, + "card_number": 338, + "year": 2001, + "card_set_id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", + "rooster_id": "f7d50eb8-b42f-4240-8bd6-2321f0d94d93", + "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" + }, + { + "id": "9c06d0a4-2091-4406-a1cc-dda3da79ced0", + "created_date": "2024-08-16 23:58:37.600000", + "last_modified_date": "2024-08-16 23:58:37.600000", + "version": 0, + "card_number": 31, + "year": 2001, + "card_set_id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", + "rooster_id": "8842ec4c-fddd-46ea-9709-a35c78441b07", + "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" + }, + { + "id": "a29beea1-3e4c-4dc7-b131-686b0af11496", + "created_date": "2024-08-16 23:58:37.592000", + "last_modified_date": "2024-08-16 23:58:37.592000", + "version": 0, + "card_number": 250, + "year": 2001, + "card_set_id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", + "rooster_id": "593e70f9-fcea-428d-8dbd-a4d4909e54ad", + "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" + }, + { + "id": "a94a9d19-9d46-403b-9561-7341ad2d2b52", + "created_date": "2024-08-16 23:58:37.597000", + "last_modified_date": "2024-08-16 23:58:37.597000", + "version": 0, + "card_number": 37, + "year": 2001, + "card_set_id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", + "rooster_id": "da266d28-01c9-4f5f-a166-d984743fa9a5", + "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" + }, + { + "id": "ac7cbab9-fe01-4cec-9b9f-4f77541d165d", + "created_date": "2024-08-16 23:58:37.599000", + "last_modified_date": "2024-08-16 23:58:37.599000", + "version": 0, + "card_number": 38, + "year": 2001, + "card_set_id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", + "rooster_id": "ad4d73ec-97c2-4f6e-8e31-fff4e38985de", + "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" + }, + { + "id": "f4c2ebca-5bfd-41cb-b8cd-71dee25c5e96", + "created_date": "2024-08-16 23:58:37.606000", + "last_modified_date": "2024-08-16 23:58:37.606000", + "version": 0, + "card_number": 345, + "year": 2001, + "card_set_id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", + "rooster_id": "c294649f-f36b-4ba2-a695-6329820332fa", + "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" + } + ], + "media_file": [ + { + "id": "00078197-3ef4-4fdf-9eeb-f4714f61c09a", + "created_date": "2024-07-25 07:29:47.548460", + "last_modified_date": "2024-11-12 22:55:14.432000", + "version": 8, + "url": "https://ge.xhamster.com/videos/family-strokes-busty-stepsis-sneaks-into-her-stepbros-room-for-a-quickie-after-passionate-shower-xhqxWO9", + "review": 0, + "should_download": 0, + "title": "Family Strokes - die vollbusige Stiefschwester schleicht sich nach einem leidenschaftlichen Duschen in das Zimmer ihres Stiefbruders | xHamster", + "file_name": "Family Strokes - die vollbusige Stiefschwester schleicht sich nach einem leidenschaftlichen Duschen in das Zimmer ihres Stiefbruders [xhqxWO9].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/00078197-3ef4-4fdf-9eeb-f4714f61c09a.mp4" + }, + { + "id": "0014d94a-6846-4abf-9fc6-2e271dbf59ad", + "created_date": "2024-07-25 07:29:45.915753", + "last_modified_date": "2024-07-25 07:29:45.915753", + "version": 0, + "url": "https://ge.xhamster.com/videos/doppelte-lust-full-german-movie-xhDRBCS", + "review": 0, + "should_download": 0, + "title": "Doppelte Lust- Full German Movie, Free Porn 39 | xHamster", + "file_name": "Doppelte Lust- full german movie [xhDRBCS].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0014d94a-6846-4abf-9fc6-2e271dbf59ad.mp4" + }, + { + "id": "0097749b-c37f-4a6d-81fe-a1906bda6b97", + "created_date": "2024-07-25 07:29:46.407645", + "last_modified_date": "2024-07-25 07:29:46.407645", + "version": 0, + "url": "https://ge.xhamster.com/videos/three-sisters-vintage-movie-clip-14390892", + "review": 0, + "should_download": 0, + "title": "Drei Schwestern (alter Filmclip) | xHamster", + "file_name": "Drei Schwestern (alter Filmclip) [14390892].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0097749b-c37f-4a6d-81fe-a1906bda6b97.mp4" + }, + { + "id": "00b13a80-cbdb-4080-962e-d6d6455f6785", + "created_date": "2024-07-25 07:29:44.997284", + "last_modified_date": "2024-07-25 07:29:44.997284", + "version": 0, + "url": "https://ge.xhamster.com/videos/endlich-sommer-endlich-anal-im-hohen-grass-xhAW2N0", + "review": 0, + "should_download": 0, + "title": "Endlich Sommer Endlich Anal Im Hohen Grass: Free HD Porn e5 | xHamster", + "file_name": "Endlich Sommer, endlich Anal im hohen Grass [xhAW2N0].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/00b13a80-cbdb-4080-962e-d6d6455f6785.mp4" + }, + { + "id": "00b9fe81-01d6-4cd8-a5db-080a835e341e", + "created_date": "2024-07-25 07:29:47.918268", + "last_modified_date": "2024-07-25 07:29:47.918268", + "version": 0, + "url": "https://ge.xhamster.com/videos/agent-aika-1-7900892", + "review": 0, + "should_download": 0, + "title": "Agent Aika 1: Free 18 Year Old Porn Video 59 | xHamster", + "file_name": "agent aika 1 [7900892].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/00b9fe81-01d6-4cd8-a5db-080a835e341e.mp4" + }, + { + "id": "00e48b0a-1f2b-462a-b20d-10c5c7d9dcbd", + "created_date": "2024-07-25 07:29:47.770607", + "last_modified_date": "2024-12-30 23:01:48.271000", + "version": 1, + "url": "https://ge.xhamster.com/videos/your-the-best-big-stepbrother-ever-madi-collins-gushes-to-her-stepbro-s22-e2-xhb49W9", + "review": 0, + "should_download": 0, + "title": "\"Du bist der beste gro\u00dfe Stiefbruder aller Zeiten!\" Madi Collins sprudelt vor ihrem Stiefbruder22: e2 | xHamster", + "file_name": "\uff02Du bist der beste gro\u00dfe Stiefbruder aller Zeiten!\uff02 Madi Collins sprudelt vor ihrem Stiefbruder22\uff1a e2 [xhb49W9].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/00e48b0a-1f2b-462a-b20d-10c5c7d9dcbd.mp4" + }, + { + "id": "0146bc3c-aa27-4eb9-872f-3af8b46c90ec", + "created_date": "2024-07-25 07:29:44.993784", + "last_modified_date": "2024-10-09 12:40:02.438000", + "version": 1, + "url": "https://ge.xhamster.com/videos/office-girls-full-movie-xhxJtYW", + "review": 0, + "should_download": 0, + "title": "B\u00fcro-M\u00e4dchen (kompletter Film) | xHamster", + "file_name": "7fd7b16a-382f-4ed6-ac1d-bdf08855641b.mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0146bc3c-aa27-4eb9-872f-3af8b46c90ec.mp4" + }, + { + "id": "0174c9b5-f4f1-42a9-997b-1ad85e42f36f", + "created_date": "2024-09-05 20:03:45.856643", + "last_modified_date": "2024-09-25 06:04:58.294000", + "version": 1, + "url": "https://ge.xhamster.com/videos/inzt-insel-xhn7u3d", + "review": 0, + "should_download": 0, + "title": "Inzt-insel: Free Threesome Porn Video 86 | xHamster", + "file_name": "Inzt-Insel [xhn7u3d].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/0174c9b5-f4f1-42a9-997b-1ad85e42f36f.mp4" + }, + { + "id": "017f226c-63b3-42a9-a451-2228809e5603", + "created_date": "2024-07-25 07:29:44.791100", + "last_modified_date": "2024-07-25 07:29:44.791100", + "version": 0, + "url": "https://ge.xhamster.com/videos/er-fickt-seine-mitbewohnerin-xhojScu", + "review": 0, + "should_download": 0, + "title": "Er Fickt Seine Mitbewohnerin, Free Tight Pussy HD Porn 9f | xHamster", + "file_name": "Er fickt seine Mitbewohnerin [xhojScu].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/017f226c-63b3-42a9-a451-2228809e5603.mp4" + }, + { + "id": "018162cc-d900-4a47-b753-467c755d805e", + "created_date": "2024-10-14 20:33:38.255531", + "last_modified_date": "2024-10-21 16:24:41.833000", + "version": 1, + "url": "https://ge.xhamster.com/videos/classic-anal-7276737", + "review": 0, + "should_download": 0, + "title": "Klassischer Analsex | xHamster", + "file_name": "Klassischer Analsex [7276737].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/018162cc-d900-4a47-b753-467c755d805e.mp4" + }, + { + "id": "01dad984-690b-424f-b9d4-7783a5de9c0b", + "created_date": "2024-07-25 07:29:46.953383", + "last_modified_date": "2024-07-25 07:29:46.953383", + "version": 0, + "url": "https://ge.xhamster.com/videos/lets-bang-my-stepmom-s20-e4-xhmJJJO", + "review": 0, + "should_download": 0, + "title": "Lass uns meine stiefmutter knallen - s20: e4 | xHamster", + "file_name": "Lass uns meine stiefmutter knallen - s20\uff1a e4 [xhmJJJO].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/01dad984-690b-424f-b9d4-7783a5de9c0b.mp4" + }, + { + "id": "01edab37-a939-411f-bc70-ea219f66867a", + "created_date": "2024-07-25 07:29:44.643317", + "last_modified_date": "2024-07-25 07:29:44.643317", + "version": 0, + "url": "https://ge.xhamster.com/videos/i-came-so-hard-i-pushed-him-out-of-me-lexi-luna-tells-lana-smalls-s7-e7-xhBJUlE", + "review": 0, + "should_download": 0, + "title": "\"Ich bin so hart gekommen, dass ich ihn aus mir gesto\u00dfen habe\" Lexi Luna sagt zu Lana Smalls - S7: E7 | xHamster", + "file_name": "\uff02Ich bin so hart gekommen, dass ich ihn aus mir gesto\u00dfen habe\uff02 Lexi Luna sagt zu Lana Smalls - S7\uff1a E7 [xhBJUlE].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/01edab37-a939-411f-bc70-ea219f66867a.mp4" + }, + { + "id": "0289b250-8ae0-48be-8ff1-35df94a8c8d3", + "created_date": "2024-07-25 07:29:46.670160", + "last_modified_date": "2024-07-25 07:29:46.670160", + "version": 0, + "url": "https://ge.xhamster.com/videos/appalled-stepmom-catches-her-stepson-touching-himself-while-his-stepsister-dances-on-the-bed-mylf-xhaJe5e", + "review": 0, + "should_download": 0, + "title": "Entsetzte stiefmutter erwischt ihren stiefsohn beim ber\u00fchren, w\u00e4hrend seine stiefschwester auf dem bett tanzt - MYLF | xHamster", + "file_name": "Entsetzte stiefmutter erwischt ihren stiefsohn beim ber\u00fchren, w\u00e4hrend seine stiefschwester auf dem bett tanzt - MYLF [xhaJe5e].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0289b250-8ae0-48be-8ff1-35df94a8c8d3.mp4" + }, + { + "id": "028cde1a-774b-4a2f-b1f6-23305d5fb17b", + "created_date": "2024-08-28 23:21:54.370386", + "last_modified_date": "2024-08-28 23:21:54.370386", + "version": 0, + "url": "https://ge.xhamster.com/videos/summer-camp-girls-1983-9356435", + "review": 0, + "should_download": 0, + "title": "Sommerlager-M\u00e4dchen (1983) | xHamster", + "file_name": "Sommerlager-M\u00e4dchen (1983) [9356435].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/028cde1a-774b-4a2f-b1f6-23305d5fb17b.mp4" + }, + { + "id": "02923b61-2065-46e7-9843-0b8c83d8e268", + "created_date": "2024-07-25 07:29:47.658565", + "last_modified_date": "2024-07-25 07:29:47.658565", + "version": 0, + "url": "https://ge.xhamster.com/videos/wunsche-und-perversionen-1976-aka-desirs-et-perversions-xhP0HAt", + "review": 0, + "should_download": 0, + "title": "Wunsche Und Perversionen 1976 Aka Desirs Et Perversions | xHamster", + "file_name": "Wunsche und Perversionen (1976) aka Desirs et perversions [xhP0HAt].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/02923b61-2065-46e7-9843-0b8c83d8e268.mp4" + }, + { + "id": "02a6b022-60d0-4273-9f0d-d8a11c4874eb", + "created_date": "2025-01-19 13:42:30.070779", + "last_modified_date": "2025-01-19 13:42:30.070785", + "version": 0, + "url": "https://ge.xhamster.com/videos/giada-and-michelle-fuck-on-the-sofa-with-friends-xhK6oKV", + "review": 0, + "should_download": 0, + "title": "Giada und Michelle ficken mit freunden auf dem Sofa | xHamster", + "file_name": "Giada und Michelle ficken mit freunden auf dem Sofa [xhK6oKV].mp4", + "path": null, + "cloud_link": null + }, + { + "id": "03384a21-14ad-4a34-9f35-87f009507018", + "created_date": "2024-07-25 07:29:46.153823", + "last_modified_date": "2024-07-25 07:29:46.153823", + "version": 0, + "url": "https://ge.xhamster.com/videos/hot-blonde-starts-a-hot-orgy-and-gets-fucked-hard-in-all-holes-xh27Wki", + "review": 0, + "should_download": 0, + "title": "hei\u00dfe Blondine startet eine hei\u00dfe Orgie und wird hart gefickt in alle l\u00f6cher | xHamster", + "file_name": "hei\u00dfe Blondine startet eine hei\u00dfe Orgie und wird hart gefickt in alle l\u00f6cher [xh27Wki].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/03384a21-14ad-4a34-9f35-87f009507018.mp4" + }, + { + "id": "03b87a54-ecfa-49a0-800c-5c63f8baa0ed", + "created_date": "2024-07-25 07:29:45.386206", + "last_modified_date": "2024-07-25 07:29:45.386206", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepdaughter-seduced-while-family-camping-with-daddy-xhRgzwd", + "review": 0, + "should_download": 0, + "title": "Stieftochter beim Familiencamping mit Papa verf\u00fchrt | xHamster", + "file_name": "Stieftochter beim Familiencamping mit Papa verf\u00fchrt [xhRgzwd].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/03b87a54-ecfa-49a0-800c-5c63f8baa0ed.mp4" + }, + { + "id": "03c0caa8-5aa3-4624-89e2-ceafc23d78d5", + "created_date": "2024-08-28 23:21:54.358831", + "last_modified_date": "2024-08-28 23:21:54.358831", + "version": 0, + "url": "https://ge.xhamster.com/videos/some-cans-make-cute-girls-feel-lusty-and-take-part-in-hot-saturnalia-xhpFkf3", + "review": 0, + "should_download": 0, + "title": "Einige Dosen lassen s\u00fc\u00dfe M\u00e4dchen lustvoll werden und nehmen an hei\u00dfen Saturnalia teil | xHamster", + "file_name": "Einige Dosen lassen s\u00fc\u00dfe M\u00e4dchen lustvoll werden und nehmen an hei\u00dfen Saturnalia teil [xhpFkf3].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/03c0caa8-5aa3-4624-89e2-ceafc23d78d5.mp4" + }, + { + "id": "03c134e9-4666-4198-b809-dd923b8f84aa", + "created_date": "2024-07-25 07:29:46.829507", + "last_modified_date": "2024-07-25 07:29:46.829507", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-godfather-comes-to-visit-and-i-convince-him-to-get-in-the-pool-until-he-finishes-me-xhGDlup", + "review": 0, + "should_download": 0, + "title": "Mein pate kommt zu besuch und ich \u00fcberrede ihn, in den pool zu kommen, bis er mich fertig macht | xHamster", + "file_name": "Mein pate kommt zu besuch und ich \u00fcberrede ihn, in den pool zu kommen, bis er mich fertig macht [xhGDlup].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/03c134e9-4666-4198-b809-dd923b8f84aa.mp4" + }, + { + "id": "0424a283-580e-439d-a197-aca981d0c17d", + "created_date": "2024-07-25 07:29:47.691177", + "last_modified_date": "2024-07-25 07:29:47.691177", + "version": 0, + "url": "https://ge.xhamster.com/videos/carter-cruise-caught-roommate-piper-perri-masturbating-while-sniffing-dirty-panties-gets-turned-on-xhYQqfd", + "review": 0, + "should_download": 0, + "title": "Carter cruise erwischt mitbewohnerin piper perri beim masturbieren am schn\u00fcffeln am schmutzigen h\u00f6schen und wird angemacht | xHamster", + "file_name": "Carter cruise erwischt mitbewohnerin piper perri beim masturbieren am schn\u00fcffeln am schmutzigen h\u00f6schen und wird angemacht [xhYQqfd].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0424a283-580e-439d-a197-aca981d0c17d.mp4" + }, + { + "id": "044bd91b-3898-4f28-b24a-956c6a7085cb", + "created_date": "2024-07-25 07:29:47.163565", + "last_modified_date": "2024-07-25 07:29:47.163565", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepmom-says-go-ahead-put-it-in-xheBKuW", + "review": 0, + "should_download": 0, + "title": "Stiefmutter sagt, mach weiter! Steck ihn rein! | xHamster", + "file_name": "Stiefmutter sagt, mach weiter! Steck ihn rein! [xheBKuW].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/044bd91b-3898-4f28-b24a-956c6a7085cb.mp4" + }, + { + "id": "044d0b60-8789-43ff-b805-8a443a459982", + "created_date": "2024-07-25 07:29:46.802774", + "last_modified_date": "2024-07-25 07:29:46.802774", + "version": 0, + "url": "https://ge.xhamster.com/videos/biologiestunde-wird-zum-gangbang-xhKxLBY", + "review": 0, + "should_download": 0, + "title": "Biologiestunde Wird Zum Gangbang, Free Porn 7b | xHamster", + "file_name": "Biologiestunde wird zum Gangbang [xhKxLBY].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/044d0b60-8789-43ff-b805-8a443a459982.mp4" + }, + { + "id": "044feea9-781e-4644-9da1-6b6c75984d87", + "created_date": "2024-07-25 07:29:45.616760", + "last_modified_date": "2024-07-25 07:29:45.616760", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsis-says-are-you-touching-yourself-and-staring-at-me-xhTGVR5", + "review": 0, + "should_download": 0, + "title": "Stiefschwester sagt, ber\u00fchrst du dich selbst und starrst mich an ?! | xHamster", + "file_name": "Stiefschwester sagt, ber\u00fchrst du dich selbst und starrst mich an \uff1f! [xhTGVR5].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/044feea9-781e-4644-9da1-6b6c75984d87.mp4" + }, + { + "id": "0465fa7a-44bf-41d7-b1d4-d01d0fab813f", + "created_date": "2024-07-25 07:29:47.734387", + "last_modified_date": "2024-07-25 07:29:47.734387", + "version": 0, + "url": "https://ge.xhamster.com/videos/2-guys-and-3-girls-playing-some-strip-games-14066218", + "review": 0, + "should_download": 0, + "title": "2 Typen und 3 M\u00e4dchen spielen Strip-Spiele | xHamster", + "file_name": "2 Typen und 3 M\u00e4dchen spielen Strip-Spiele [14066218].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0465fa7a-44bf-41d7-b1d4-d01d0fab813f.mp4" + }, + { + "id": "047f8800-aa71-4177-8f6b-bdde9bdf5516", + "created_date": "2024-08-09 21:31:03.811829", + "last_modified_date": "2024-09-06 09:41:50.230000", + "version": 3, + "url": "https://ge.xhamster.com/videos/first-love-episode-1-xhGLIaq", + "review": 0, + "should_download": 0, + "title": "Video in \u00dcberpr\u00fcfung", + "file_name": "Erste liebe episode 1 [xhGLIaq].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/047f8800-aa71-4177-8f6b-bdde9bdf5516.mp4" + }, + { + "id": "048855b3-bf66-4913-a963-1f39ada62cc2", + "created_date": "2024-07-25 07:29:44.699379", + "last_modified_date": "2024-07-25 07:29:44.699379", + "version": 0, + "url": "https://ge.xhamster.com/videos/flashing-my-pussy-in-front-of-a-man-in-public-beach-and-he-helps-me-squirt-its-very-risky-misscreamy-xhqH4q3", + "review": 0, + "should_download": 0, + "title": "Ich zeige meine Muschi vor einem Mann am \u00f6ffentlichen Strand und er hilft mir beim Spritzen \u2013 das ist sehr riskant \u2013 MissCreamy | xHamster", + "file_name": "Ich zeige meine Muschi vor einem Mann am \u00f6ffentlichen Strand und er hilft mir beim Spritzen \u2013 das ist sehr riskant \u2013 MissCreamy [xhqH4q3].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/048855b3-bf66-4913-a963-1f39ada62cc2.mp4" + }, + { + "id": "04e75137-e86d-4fa3-be27-b0c0bcbbf6e3", + "created_date": "2024-11-01 21:08:26.930541", + "last_modified_date": "2024-11-01 21:08:26.930541", + "version": 0, + "url": "https://ge.xhamster.com/videos/best-of-1159-11057365", + "review": 0, + "should_download": 0, + "title": "Das Beste von # 1159 | xHamster", + "file_name": "Das Beste von # 1159 [11057365].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/04e75137-e86d-4fa3-be27-b0c0bcbbf6e3.mp4" + }, + { + "id": "04ee7dd7-cf7a-445e-ab0b-bb74298191da", + "created_date": "2024-07-25 07:29:46.370333", + "last_modified_date": "2024-07-25 07:29:46.370333", + "version": 0, + "url": "https://ge.xhamster.com/videos/heidi-spritzbuben-der-berge-teil-5-8129114", + "review": 0, + "should_download": 0, + "title": "Heidi Spritzbuben Der Berge Teil 5, Free Porn d0 | xHamster", + "file_name": "Heidi Spritzbuben Der Berge Teil 5 [8129114].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/04ee7dd7-cf7a-445e-ab0b-bb74298191da.mp4" + }, + { + "id": "05075485-bcf6-46a3-8b68-084b3c7ab55c", + "created_date": "2024-07-25 07:29:46.795435", + "last_modified_date": "2024-07-25 07:29:46.795435", + "version": 0, + "url": "https://ge.xhamster.com/videos/mama-rewards-two-boys-hard-work-with-hot-dp-anal-action-2041440", + "review": 0, + "should_download": 0, + "title": "Mama belohnt zwei Jungs harte Arbeit mit hei\u00dfer Doppelpenetration !! | xHamster", + "file_name": "Mama belohnt zwei Jungs harte Arbeit mit hei\u00dfer Doppelpenetration !! [2041440].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/05075485-bcf6-46a3-8b68-084b3c7ab55c.mp4" + }, + { + "id": "051d862c-6eef-4f67-972c-6221174e1609", + "created_date": "2024-10-07 20:47:56.417683", + "last_modified_date": "2024-10-07 20:47:56.417683", + "version": 0, + "url": "https://ge.xhamster.com/videos/public-beach-sex-in-spain-everyone-can-finger-and-fuck-me-on-the-beach-xhSydAg", + "review": 0, + "should_download": 0, + "title": "Jeder darf mich am Strand fingern und ficken | xHamster", + "file_name": "Jeder darf mich am Strand fingern und ficken [xhSydAg].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/051d862c-6eef-4f67-972c-6221174e1609.mp4" + }, + { + "id": "05221b06-fd7b-4159-90de-083e546b5816", + "created_date": "2024-07-25 07:29:47.555619", + "last_modified_date": "2024-07-25 07:29:47.555619", + "version": 0, + "url": "https://ge.xhamster.com/videos/do-you-know-how-to-use-your-dick-better-than-a-tennis-racket-milf-cory-chase-asks-stepson-s19-e9-xhNyi3I", + "review": 0, + "should_download": 0, + "title": "\"Wei\u00dft du, wie man deinen schwanz besser benutzt als ein Tennisschl\u00e4ger?\" Milf cory chase fragt stiefsohn - S19: E9 | xHamster", + "file_name": "\uff02Wei\u00dft du, wie man deinen schwanz besser benutzt als ein Tennisschl\u00e4ger\uff1f\uff02 Milf cory chase fragt stiefsohn - S19\uff1a E9 [xhNyi3I].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/05221b06-fd7b-4159-90de-083e546b5816.mp4" + }, + { + "id": "05253103-25ff-4b2e-aab3-d7d2c5ec2b73", + "created_date": "2024-07-25 07:29:46.196509", + "last_modified_date": "2024-07-25 07:29:46.196509", + "version": 0, + "url": "https://ge.xhamster.com/videos/cream-pie-orgies-fuck-groups-threesomes-hardcore-gangbangs-13477993", + "review": 0, + "should_download": 0, + "title": "Creampie-Orgien, ficken, Hardcore-Gangbangs, Dreier | xHamster", + "file_name": "Creampie-Orgien, ficken, Hardcore-Gangbangs, Dreier [13477993].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/05253103-25ff-4b2e-aab3-d7d2c5ec2b73.mp4" + }, + { + "id": "059c5f52-3db4-4a2c-bab4-ac3de1b8d310", + "created_date": "2024-07-25 07:29:46.568972", + "last_modified_date": "2024-07-25 07:29:46.568972", + "version": 0, + "url": "https://ge.xhamster.com/videos/pfadfinderrinnen-12311349", + "review": 0, + "should_download": 0, + "title": "Pfadfinderrinnen: Free European Porn Video 45 | xHamster", + "file_name": "Pfadfinderrinnen [12311349].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/059c5f52-3db4-4a2c-bab4-ac3de1b8d310.mp4" + }, + { + "id": "05ceaeb9-5e2d-4c86-8146-135ebbd56960", + "created_date": "2024-07-25 07:29:46.806524", + "last_modified_date": "2024-07-25 07:29:46.806524", + "version": 0, + "url": "https://ge.xhamster.com/videos/schulmadchen-report-11-1976-5777639", + "review": 0, + "should_download": 0, + "title": "Schulmadchen-Bericht 11 (1976) | xHamster", + "file_name": "Schulmadchen-Bericht 11 (1976) [5777639].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/05ceaeb9-5e2d-4c86-8146-135ebbd56960.mp4" + }, + { + "id": "060ca759-b628-41f2-a5b5-1f40714bced8", + "created_date": "2024-12-29 23:53:27.912424", + "last_modified_date": "2024-12-29 23:53:27.912424", + "version": 0, + "url": "https://ge.xhamster.com/videos/sex-internet-xhjFMjI", + "review": 0, + "should_download": 0, + "title": "Sex & Internet | xHamster", + "file_name": "Sex & Internet [xhjFMjI].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/060ca759-b628-41f2-a5b5-1f40714bced8.mp4" + }, + { + "id": "0659eb79-4a30-4f1b-bfe5-78e99ba9a1af", + "created_date": "2024-07-25 07:29:45.252618", + "last_modified_date": "2024-07-25 07:29:45.252618", + "version": 0, + "url": "https://ge.xhamster.com/videos/full-fun-sex-xhzemUc", + "review": 0, + "should_download": 0, + "title": "Voller Spa\u00df-Sex | xHamster", + "file_name": "Voller Spa\u00df-Sex [xhzemUc].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0659eb79-4a30-4f1b-bfe5-78e99ba9a1af.mp4" + }, + { + "id": "06b582ed-4910-474a-bd27-87495dc5fe4f", + "created_date": "2024-07-25 07:29:45.732777", + "last_modified_date": "2024-07-25 07:29:45.732777", + "version": 0, + "url": "https://ge.xhamster.com/videos/mom-loves-dp-and-his-cock-is-not-to-big-for-her-ass-xhGX68d", + "review": 0, + "should_download": 0, + "title": "MUTTER liebt doppelpenetration und sein schwanz ist nicht zu gro\u00df f\u00fcr ihren ARSCH | xHamster", + "file_name": "MUTTER liebt doppelpenetration und sein schwanz ist nicht zu gro\u00df f\u00fcr ihren ARSCH [xhGX68d].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/06b582ed-4910-474a-bd27-87495dc5fe4f.mp4" + }, + { + "id": "06e26b80-9fb1-4727-ab1c-123260f8967f", + "created_date": "2024-07-25 07:29:47.828611", + "last_modified_date": "2024-07-25 07:29:47.828611", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-stepsister-loves-it-when-i-watch-her-in-the-bathroom-annahomemix-xhfj4po", + "review": 0, + "should_download": 0, + "title": "Meine Stiefschwester liebt es, wenn ich sie im Badezimmer beobachte. annahomemix | xHamster", + "file_name": "Meine Stiefschwester liebt es, wenn ich sie im Badezimmer beobachte. annahomemix [xhfj4po].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/06e26b80-9fb1-4727-ab1c-123260f8967f.mp4" + }, + { + "id": "0782eede-780c-442f-8787-74de7bf0af6c", + "created_date": "2024-09-24 08:11:39.008987", + "last_modified_date": "2024-10-21 16:24:48.315000", + "version": 1, + "url": "https://ge.xhamster.com/videos/babes-step-mom-lessons-cozy-by-the-fire-starring-jay-smo-8274351", + "review": 0, + "should_download": 0, + "title": "Babes - Stiefmutter-Unterricht - gem\u00fctlich am Feuer mit Jay Smo | xHamster", + "file_name": "Babes - Stiefmutter-Unterricht - gem\u00fctlich am Feuer mit Jay Smo [8274351].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/0782eede-780c-442f-8787-74de7bf0af6c.mp4" + }, + { + "id": "07ad27c7-4fef-4c0e-8a27-7a2edf59fc49", + "created_date": "2024-08-16 11:12:31.006000", + "last_modified_date": "2024-10-21 16:24:56.197000", + "version": 1, + "url": "https://ge.xhamster.com/videos/okay-you-can-fuck-me-in-the-ass-but-please-be-quiet-so-that-my-stepmother-doesnt-hear-you-xhqLrKI", + "review": 0, + "should_download": 0, + "title": "Okay, Du kannst mich in den arsch ficken, aber bitte sei ruhig, damit meine stiefmutter dich nicht h\u00f6rt! | xHamster", + "file_name": "Okay, Du kannst mich in den arsch ficken, aber bitte sei ruhig, damit meine stiefmutter dich nicht h\u00f6rt! [xhqLrKI].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/07ad27c7-4fef-4c0e-8a27-7a2edf59fc49.mp4" + }, + { + "id": "07b14ed2-d5c7-4f1e-aea6-c8d1b58292ae", + "created_date": "2024-07-25 07:29:44.363850", + "last_modified_date": "2024-07-25 07:29:44.363850", + "version": 0, + "url": "https://ge.xhamster.com/videos/big-breasted-redhead-step-sister-loves-brother-15017208", + "review": 0, + "should_download": 0, + "title": "Vollbusige rothaarige Stiefschwester liebt Bruder | xHamster", + "file_name": "Vollbusige rothaarige Stiefschwester liebt Bruder [15017208].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/07b14ed2-d5c7-4f1e-aea6-c8d1b58292ae.mp4" + }, + { + "id": "07c09354-86dd-4e7d-9c00-30beeb1f7f1a", + "created_date": "2024-07-25 07:29:46.745501", + "last_modified_date": "2024-07-25 07:29:46.745501", + "version": 0, + "url": "https://ge.xhamster.com/videos/super-funny-pantsing-games-xh1m7oX", + "review": 0, + "should_download": 0, + "title": "Super lustige H\u00f6schen-Spiele | xHamster", + "file_name": "Super lustige H\u00f6schen-Spiele [xh1m7oX].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/07c09354-86dd-4e7d-9c00-30beeb1f7f1a.mp4" + }, + { + "id": "07c8a142-32e6-4bc0-9d35-abf73d992f8a", + "created_date": "2024-07-25 07:29:47.360864", + "last_modified_date": "2024-07-25 07:29:47.360864", + "version": 0, + "url": "https://ge.xhamster.com/videos/2-guys-and-not-their-sisters-in-the-jacuzzi-4387561", + "review": 0, + "should_download": 0, + "title": "2 Typen und nicht ihre Schwestern im Whirlpool | xHamster", + "file_name": "2 Typen und nicht ihre Schwestern im Whirlpool [4387561].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/07c8a142-32e6-4bc0-9d35-abf73d992f8a.mp4" + }, + { + "id": "07d5caf8-5cf2-41dc-81f5-f703485ae863", + "created_date": "2024-07-25 07:29:47.070657", + "last_modified_date": "2024-07-25 07:29:47.070657", + "version": 0, + "url": "https://ge.xhamster.com/videos/sneaky-sex-nick-moreno-lana-roy-silent-retreat-13704297", + "review": 0, + "should_download": 0, + "title": "Hinterh\u00e4ltiger Sex - Nick Moreno Lana Roy - stiller R\u00fcckzug | xHamster", + "file_name": "Hinterh\u00e4ltiger Sex - Nick Moreno Lana Roy - stiller R\u00fcckzug [13704297].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/07d5caf8-5cf2-41dc-81f5-f703485ae863.mp4" + }, + { + "id": "082f4f8c-ce2b-46b1-a2bd-7558bfe828c5", + "created_date": "2024-07-25 07:29:46.622404", + "last_modified_date": "2024-07-25 07:29:46.622404", + "version": 0, + "url": "https://ge.xhamster.com/videos/full-swap-with-my-wife-and-her-best-friend-and-her-husband-11882483", + "review": 0, + "should_download": 0, + "title": "Voller Tausch mit meiner Frau und ihrer besten Freundin und ihrem Ehemann | xHamster", + "file_name": "Voller Tausch mit meiner Frau und ihrer besten Freundin und ihrem Ehemann [11882483].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/082f4f8c-ce2b-46b1-a2bd-7558bfe828c5.mp4" + }, + { + "id": "085389d0-720c-4a06-a009-1f062f5c9114", + "created_date": "2024-07-25 07:29:48.139594", + "last_modified_date": "2024-07-25 07:29:48.139594", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-college-friends-have-an-orgy-12765094", + "review": 0, + "should_download": 0, + "title": "Meine College-Freunde haben eine Orgie | xHamster", + "file_name": "Meine College-Freunde haben eine Orgie [12765094].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/085389d0-720c-4a06-a009-1f062f5c9114.mp4" + }, + { + "id": "088c45f9-52a8-4293-8e09-c031745327a4", + "created_date": "2024-07-25 07:29:47.930540", + "last_modified_date": "2024-07-25 07:29:47.930540", + "version": 0, + "url": "https://ge.xhamster.com/videos/american-college-xxx-the-originalin-hd-story-n-20-xhVOWVl", + "review": 0, + "should_download": 0, + "title": "Amerikanisches College xxx !!! - (das Original in HD) - Geschichte n. # 20 | xHamster", + "file_name": "Amerikanisches College xxx !!! - (das Original in HD) - Geschichte n. # 20 [xhVOWVl].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/088c45f9-52a8-4293-8e09-c031745327a4.mp4" + }, + { + "id": "0898b664-fa92-425d-81ee-b924f56ea4c9", + "created_date": "2024-07-25 07:29:46.432582", + "last_modified_date": "2024-07-25 07:29:46.432582", + "version": 0, + "url": "https://ge.xhamster.com/videos/great-meeting-of-the-depraved-xhEahJr", + "review": 0, + "should_download": 0, + "title": "Gro\u00dfes treffen der verdorbenen | xHamster", + "file_name": "Gro\u00dfes treffen der verdorbenen [xhEahJr].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0898b664-fa92-425d-81ee-b924f56ea4c9.mp4" + }, + { + "id": "08adc827-136e-458c-a839-03070911d4ac", + "created_date": "2024-07-25 07:29:45.075574", + "last_modified_date": "2024-09-06 09:40:44.255000", + "version": 1, + "url": "https://ge.xhamster.com/videos/hot-teen-loses-bet-now-she-fucks-her-stepbrothers-threesome-xhGaKnm", + "review": 0, + "should_download": 0, + "title": "Hei\u00dfes Teen verliert Wette, jetzt fickt sie ihre Stiefbr\u00fcder! ", + "file_name": "Hei\u00dfes Teen verliert Wette, jetzt fickt sie ihre Stiefbr\u00fcder! Dreier [xhGaKnm].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/08adc827-136e-458c-a839-03070911d4ac.mp4" + }, + { + "id": "08fea4ec-bb00-4d7b-8114-d8740033e5c9", + "created_date": "2025-01-19 13:42:38.334114", + "last_modified_date": "2025-01-19 13:42:38.334120", + "version": 0, + "url": "https://ge.xhamster.com/videos/shocked-stepsons-catch-their-mylf-stepmothers-acting-like-dirty-perverts-momswap-xhYY3Lc", + "review": 0, + "should_download": 0, + "title": "Schockierte stiefsohn erwischen ihre MYLF-stiefmutter, die sich wie schmutzige perverse benimmt - momSwap | xHamster", + "file_name": "Schockierte stiefsohn erwischen ihre MYLF-stiefmutter, die sich wie schmutzige perverse benimmt - momSwap [xhYY3Lc].mp4", + "path": null, + "cloud_link": null + }, + { + "id": "09081728-1fc7-41ae-a108-c89814b4f722", + "created_date": "2024-11-10 16:53:33.485868", + "last_modified_date": "2024-11-10 16:53:33.485868", + "version": 0, + "url": "https://ge.xhamster.com/videos/hypnotized-in-laws-11169925", + "review": 0, + "should_download": 0, + "title": "Hypnotized In-laws: Free Latina HD Porn Video b5 | xHamster", + "file_name": "hypnotized in-laws [11169925].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/09081728-1fc7-41ae-a108-c89814b4f722.mp4" + }, + { + "id": "0918b266-a28d-448c-95e0-81f75831e1c9", + "created_date": "2024-07-25 07:29:46.542912", + "last_modified_date": "2024-07-25 07:29:46.542912", + "version": 0, + "url": "https://ge.xhamster.com/videos/jade-sin-gets-double-stuffed-by-white-and-black-dicks-in-a-threesome-xhp3k6H", + "review": 0, + "should_download": 0, + "title": "Jade Sin wird in einem Dreier doppelt von wei\u00dfen und schwarzen Schw\u00e4nzen gestopft | xHamster", + "file_name": "Jade Sin wird in einem Dreier doppelt von wei\u00dfen und schwarzen Schw\u00e4nzen gestopft [xhp3k6H].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0918b266-a28d-448c-95e0-81f75831e1c9.mp4" + }, + { + "id": "09827dd9-888d-413f-9082-42c96cfb157b", + "created_date": "2024-07-25 07:29:45.907625", + "last_modified_date": "2024-07-25 07:29:45.907625", + "version": 0, + "url": "https://ge.xhamster.com/videos/summer-camp-sun-bunnies-2003-xhS9UMh", + "review": 0, + "should_download": 0, + "title": "Sommerlager Sonnenhasen (2003) | xHamster", + "file_name": "Sommerlager Sonnenhasen (2003) [xhS9UMh].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/09827dd9-888d-413f-9082-42c96cfb157b.mp4" + }, + { + "id": "0989ac7f-656f-4456-a7ba-d333d4f87268", + "created_date": "2024-07-25 07:29:44.428513", + "last_modified_date": "2024-07-25 07:29:44.428513", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-kings-servants-6234783", + "review": 0, + "should_download": 0, + "title": "Die Diener der K\u00f6nige | xHamster", + "file_name": "Die Diener der K\u00f6nige [6234783].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0989ac7f-656f-4456-a7ba-d333d4f87268.mp4" + }, + { + "id": "09bd3e92-73dc-46dd-97cf-36de79c7520c", + "created_date": "2024-07-25 07:29:44.655312", + "last_modified_date": "2024-07-25 07:29:44.655312", + "version": 0, + "url": "https://ge.xhamster.com/videos/verschollen-full-movie-xhjxenK", + "review": 0, + "should_download": 0, + "title": "Verschollen Full Movie, Free Story Porn Video 31 | xHamster", + "file_name": "Verschollen (Full Movie) [xhjxenK].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/09bd3e92-73dc-46dd-97cf-36de79c7520c.mp4" + }, + { + "id": "09c2c426-322d-4d2a-aa9c-07dc088dbda9", + "created_date": "2024-10-07 20:47:56.412412", + "last_modified_date": "2024-10-21 16:25:02.666000", + "version": 1, + "url": "https://ge.xhamster.com/videos/threesome-on-couch-4447753", + "review": 0, + "should_download": 0, + "title": "Dreier auf der Couch | xHamster", + "file_name": "Dreier auf der Couch [4447753].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/09c2c426-322d-4d2a-aa9c-07dc088dbda9.mp4" + }, + { + "id": "0a238e83-e2fc-40ab-aa8e-3a80b66e3e88", + "created_date": "2024-07-25 07:29:45.060515", + "last_modified_date": "2024-07-25 07:29:45.060515", + "version": 0, + "url": "https://ge.xhamster.com/videos/die-wette-1009249", + "review": 0, + "should_download": 0, + "title": "Die Wette: Free Anal & Amateur Porn Video e8 | xHamster", + "file_name": "Die Wette ! [1009249].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0a238e83-e2fc-40ab-aa8e-3a80b66e3e88.mp4" + }, + { + "id": "0a2f9d22-71fe-4551-bd40-c7850155ac2c", + "created_date": "2024-07-25 07:29:44.409917", + "last_modified_date": "2024-07-25 07:29:44.409917", + "version": 0, + "url": "https://ge.xhamster.com/videos/18-and-confused-7-xha5g9P", + "review": 0, + "should_download": 0, + "title": "18 und verwirrt # 7 | xHamster", + "file_name": "18 und verwirrt # 7 [xha5g9P].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0a2f9d22-71fe-4551-bd40-c7850155ac2c.mp4" + }, + { + "id": "0a305c25-9c39-45c1-a52c-7ed8e7bbd235", + "created_date": "2024-11-10 16:53:33.484084", + "last_modified_date": "2024-11-10 16:53:33.484084", + "version": 0, + "url": "https://ge.xhamster.com/videos/foursome-between-friends-xhxjxiO", + "review": 0, + "should_download": 0, + "title": "Vierer zwischen freunden | xHamster", + "file_name": "Vierer zwischen freunden [xhxjxiO].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/0a305c25-9c39-45c1-a52c-7ed8e7bbd235.mp4" + }, + { + "id": "0a43e867-a723-4e9e-b832-12a9116b18c5", + "created_date": "2024-08-28 23:21:54.379033", + "last_modified_date": "2024-08-28 23:21:54.379033", + "version": 0, + "url": "https://ge.xhamster.com/videos/what-are-neighbors-for-s18-e7-xhreDis", + "review": 0, + "should_download": 0, + "title": "Wof\u00fcr sind Nachbarn - S18: E7 | xHamster", + "file_name": "Wof\u00fcr sind Nachbarn - S18\uff1a E7 [xhreDis].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/0a43e867-a723-4e9e-b832-12a9116b18c5.mp4" + }, + { + "id": "0a6b7244-f1e5-4e1f-b513-5452ef6e521f", + "created_date": "2024-07-25 07:29:47.145407", + "last_modified_date": "2024-07-25 07:29:47.145407", + "version": 0, + "url": "https://ge.xhamster.com/videos/lovely-girlfriends-spend-the-sunny-day-sucking-and-fucking-xhQwgwY", + "review": 0, + "should_download": 0, + "title": "Sch\u00f6ne Freundinnen verbringen den sonnigen Tag mit Lutschen und Ficken | xHamster", + "file_name": "Sch\u00f6ne Freundinnen verbringen den sonnigen Tag mit Lutschen und Ficken [xhQwgwY].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0a6b7244-f1e5-4e1f-b513-5452ef6e521f.mp4" + }, + { + "id": "0a873200-41c8-41cf-ba62-90cea90fc3d4", + "created_date": "2024-09-24 08:11:38.997987", + "last_modified_date": "2024-10-21 16:25:11.331000", + "version": 1, + "url": "https://ge.xhamster.com/videos/busty-stepmoms-catch-their-respective-stepsons-masturbating-theyre-impressed-by-their-huge-cocks-xhz0hGb", + "review": 0, + "should_download": 0, + "title": "Vollbusige stiefmutter erwischen ihren jeweiligen stiefsohn beim masturbieren und sie sind von ihren riesigen schw\u00e4nzen beeindruckt | xHamster", + "file_name": "Vollbusige stiefmutter erwischen ihren jeweiligen stiefsohn beim masturbieren und sie sind von ihren riesigen schw\u00e4nzen beeindruckt [xhz0hGb].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/0a873200-41c8-41cf-ba62-90cea90fc3d4.mp4" + }, + { + "id": "0b477192-51bc-4af5-8b50-418255f71b4d", + "created_date": "2024-07-25 07:29:47.797765", + "last_modified_date": "2024-07-25 07:29:47.797765", + "version": 0, + "url": "https://ge.xhamster.com/videos/inzt-familie-1-xhhXHZ1", + "review": 0, + "should_download": 0, + "title": "Inzt Familie 1: Free Threesome Porn Video b9 | xHamster", + "file_name": "Inzt.Familie 1 [xhhXHZ1].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0b477192-51bc-4af5-8b50-418255f71b4d.mp4" + }, + { + "id": "0b4e600e-3083-49ae-ad01-9579fde1f7d1", + "created_date": "2024-07-25 07:29:46.861883", + "last_modified_date": "2024-07-25 07:29:46.861883", + "version": 0, + "url": "https://ge.xhamster.com/videos/der-geilen-chef-sekretaerin-auf-die-brille-gespritzt-xhnKQzE", + "review": 0, + "should_download": 0, + "title": "Der Geilen Chef Sekretaerin Auf Die Brille Gespritzt | xHamster", + "file_name": "Der geilen Chef Sekretaerin auf die Brille gespritzt [xhnKQzE].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0b4e600e-3083-49ae-ad01-9579fde1f7d1.mp4" + }, + { + "id": "0b7523f7-fe0f-4e47-be8d-6768ff8b6ae3", + "created_date": "2024-07-25 07:29:46.824900", + "last_modified_date": "2024-07-25 07:29:46.824900", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepbrother-stepsister-fuck-best-friend-family-therapy-14986252", + "review": 0, + "should_download": 0, + "title": "Stiefbruder und Stiefschwester ficken besten Freund - Familientherapie | xHamster", + "file_name": "Stiefbruder und Stiefschwester ficken besten Freund - Familientherapie [14986252].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0b7523f7-fe0f-4e47-be8d-6768ff8b6ae3.mp4" + }, + { + "id": "0bb44610-368e-4624-9170-d11bebed6e46", + "created_date": "2024-07-25 07:29:46.091710", + "last_modified_date": "2024-07-25 07:29:46.091710", + "version": 0, + "url": "https://ge.xhamster.com/videos/take-my-love-full-movie-xhnTUCK", + "review": 0, + "should_download": 0, + "title": "Nimm meine Liebe (kompletter Film) | xHamster", + "file_name": "Nimm meine Liebe (kompletter Film) [xhnTUCK].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0bb44610-368e-4624-9170-d11bebed6e46.mp4" + }, + { + "id": "0c1b1031-5b16-4767-a6a8-4b7734be7ed6", + "created_date": "2024-08-08 01:01:27.749190", + "last_modified_date": "2025-01-03 00:56:29.149000", + "version": 2, + "url": "", + "review": 0, + "should_download": 0, + "title": "", + "file_name": "Enjoying the lake-12246817.mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0c1b1031-5b16-4767-a6a8-4b7734be7ed6.mp4" + }, + { + "id": "0c207e34-c83e-49f0-a9a6-0b248f81c745", + "created_date": "2024-07-25 07:29:45.658141", + "last_modified_date": "2024-07-25 07:29:45.658141", + "version": 0, + "url": "https://ge.xhamster.com/videos/bitch-on-the-beach-stranger-girl-sucked-me-in-public-xh6pEaa", + "review": 0, + "should_download": 0, + "title": "Schlampe am Strand: Fremdes M\u00e4dchen hat mich in der \u00d6ffentlichkeit gelutscht | xHamster", + "file_name": "Schlampe am Strand\uff1a Fremdes M\u00e4dchen hat mich in der \u00d6ffentlichkeit gelutscht [xh6pEaa].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0c207e34-c83e-49f0-a9a6-0b248f81c745.mp4" + }, + { + "id": "0c25d694-970e-40c9-950b-23a4ef93fc81", + "created_date": "2024-07-25 07:29:47.410397", + "last_modified_date": "2024-07-25 07:29:47.410397", + "version": 0, + "url": "https://ge.xhamster.com/videos/dirty-flix-caty-kiss-hot-stepmom-teaches-teen-to-fuck-xhOF1PG", + "review": 0, + "should_download": 0, + "title": "Dirty Flix - Caty Kiss - hei\u00dfe Stiefmutter lehrt Teenager zu ficken | xHamster", + "file_name": "Dirty Flix - Caty Kiss - hei\u00dfe Stiefmutter lehrt Teenager zu ficken [xhOF1PG].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0c25d694-970e-40c9-950b-23a4ef93fc81.mp4" + }, + { + "id": "0c50a81e-4b37-436e-9c08-89ad6b42b6c1", + "created_date": "2024-12-29 23:53:27.920603", + "last_modified_date": "2024-12-29 23:53:27.920603", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepbro-says-how-about-i-make-it-even-and-show-you-my-junk-xhkEeul", + "review": 0, + "should_download": 0, + "title": "Stepbro sagt, wie w\u00e4re es, wenn ich es gerade mache und dir meinen M\u00fcll zeige | xHamster", + "file_name": "Stepbro sagt, wie w\u00e4re es, wenn ich es gerade mache und dir meinen M\u00fcll zeige [xhkEeul].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/0c50a81e-4b37-436e-9c08-89ad6b42b6c1.mp4" + }, + { + "id": "0c73ce1b-a6ac-4f51-aed9-f94dcda7f763", + "created_date": "2024-08-09 19:32:55.707863", + "last_modified_date": "2024-08-16 10:27:02.744000", + "version": 1, + "url": "https://ge.xhamster.com/videos/a-sexy-strip-memory-game-turns-sexual-real-fast-with-the-lucky-losers-xhVrqcy", + "review": 0, + "should_download": 0, + "title": "Ein sexy strip-memory-spiel wird mit den gl\u00fccklichen verlierern sexuell | xHamster", + "file_name": "Ein sexy strip-memory-spiel wird mit den gl\u00fccklichen verlierern sexuell [xhVrqcy].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/0c73ce1b-a6ac-4f51-aed9-f94dcda7f763.mp4" + }, + { + "id": "0c74b46d-1692-4d36-9ded-3629899c632a", + "created_date": "2024-07-25 07:29:46.008381", + "last_modified_date": "2024-07-25 07:29:46.008381", + "version": 0, + "url": "https://ge.xhamster.com/videos/threesome-full-of-blowjobs-and-pussy-licking-3-xhznxzT", + "review": 0, + "should_download": 0, + "title": "Dreier mit Blowjobs und Muschi lecken - 3 | xHamster", + "file_name": "Dreier mit Blowjobs und Muschi lecken - 3 [xhznxzT].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0c74b46d-1692-4d36-9ded-3629899c632a.mp4" + }, + { + "id": "0c81f67a-d98f-4b12-a9c1-9a2c32a96d38", + "created_date": "2024-07-25 07:29:47.117591", + "last_modified_date": "2024-07-25 07:29:47.117591", + "version": 0, + "url": "https://ge.xhamster.com/videos/crazy-birthday-party-spiced-up-with-group-sex-games-1409430", + "review": 0, + "should_download": 0, + "title": "Verr\u00fcckte Geburtstagsfeier, aufgepeppt mit Gruppensex-Spielen | xHamster", + "file_name": "Verr\u00fcckte Geburtstagsfeier, aufgepeppt mit Gruppensex-Spielen [1409430].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0c81f67a-d98f-4b12-a9c1-9a2c32a96d38.mp4" + }, + { + "id": "0c8dda2b-7e8f-41af-850b-9fa6a22b3c2d", + "created_date": "2024-07-25 07:29:44.839063", + "last_modified_date": "2024-07-25 07:29:44.839063", + "version": 0, + "url": "https://ge.xhamster.com/videos/big-boobed-babysitter-desperate-for-cock-maxim-law-xhTLNrm", + "review": 0, + "should_download": 0, + "title": "Vollbusige Babysitterin, verzweifelt nach Schwanz - Maxime - Gesetz | xHamster", + "file_name": "Vollbusige Babysitterin, verzweifelt nach Schwanz - Maxime - Gesetz [xhTLNrm].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0c8dda2b-7e8f-41af-850b-9fa6a22b3c2d.mp4" + }, + { + "id": "0c97f71e-d550-4261-9a7c-995527f8ece3", + "created_date": "2024-12-30 18:50:10.044000", + "last_modified_date": "2025-01-03 00:24:39.724000", + "version": 1, + "url": "https://ge.xhamster.com/videos/wife-likes-to-suck-two-dicks-and-get-fucked-by-two-guys-xhH12ga", + "review": 0, + "should_download": 0, + "title": "Ehefrau mag es, zwei schw\u00e4nze zu lutschen und von zwei typen gefickt zu werden | xHamster", + "file_name": "Ehefrau mag es, zwei schw\u00e4nze zu lutschen und von zwei typen gefickt zu werden [xhH12ga].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/0c97f71e-d550-4261-9a7c-995527f8ece3.mp4" + }, + { + "id": "0cd93120-e3f9-45a3-acd9-83467501f20d", + "created_date": "2024-07-25 07:29:47.288352", + "last_modified_date": "2024-07-25 07:29:47.288352", + "version": 0, + "url": "https://ge.xhamster.com/videos/mom-caught-stepdaughter-and-teaches-her-sex-with-bbc-in-3some-xhsIj42", + "review": 0, + "should_download": 0, + "title": "Mutter erwischt Stief Tochter beim Fummeln und fickt im Dreier mit | xHamster", + "file_name": "Mutter erwischt Stief Tochter beim Fummeln und fickt im Dreier mit [xhsIj42].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0cd93120-e3f9-45a3-acd9-83467501f20d.mp4" + }, + { + "id": "0d07b1e6-b4ac-4401-8977-14aace4002ae", + "created_date": "2024-07-25 07:29:45.029452", + "last_modified_date": "2024-07-25 07:29:45.029452", + "version": 0, + "url": "https://ge.xhamster.com/videos/wild-german-babe-gets-double-penetrated-on-the-beach-xhNxJut", + "review": 0, + "should_download": 0, + "title": "Wildes deutsches Sch\u00e4tzchen wird am Strand doppelt penetriert | xHamster", + "file_name": "Wildes deutsches Sch\u00e4tzchen wird am Strand doppelt penetriert [xhNxJut].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0d07b1e6-b4ac-4401-8977-14aace4002ae.mp4" + }, + { + "id": "0d07c158-208e-404f-8856-cb5f1fe0315e", + "created_date": "2024-07-25 07:29:45.199386", + "last_modified_date": "2024-07-25 07:29:45.199386", + "version": 0, + "url": "https://ge.xhamster.com/videos/kelly-the-coed-7-xhyi1qn", + "review": 0, + "should_download": 0, + "title": "Kelly the Coed # 7 | xHamster", + "file_name": "Kelly the Coed # 7 [xhyi1qn].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/0d07c158-208e-404f-8856-cb5f1fe0315e.mp4" + }, + { + "id": "0de0991e-00dd-4b29-86ba-319ae216a52e", + "created_date": "2024-07-25 07:29:46.162594", + "last_modified_date": "2024-07-25 07:29:46.162594", + "version": 0, + "url": "https://ge.xhamster.com/videos/daddy4k-about-gentlemans-bout-xhIBo3P", + "review": 0, + "should_download": 0, + "title": "DADDY4K. \u00dcber gentleman's blowjob | xHamster", + "file_name": "DADDY4K. \u00dcber gentleman's blowjob [xhIBo3P].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0de0991e-00dd-4b29-86ba-319ae216a52e.mp4" + }, + { + "id": "0de349c2-2ad0-4906-9d00-e67621089154", + "created_date": "2024-07-25 07:29:48.085875", + "last_modified_date": "2024-07-25 07:29:48.085875", + "version": 0, + "url": "https://ge.xhamster.com/videos/young-nympho-wants-to-experiment-in-the-pool-and-fuck-wildly-xhwEMCn", + "review": 0, + "should_download": 0, + "title": "Junge Nymphomanin will im Pool experimentieren und wild ficken | xHamster", + "file_name": "Junge Nymphomanin will im Pool experimentieren und wild ficken [xhwEMCn].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0de349c2-2ad0-4906-9d00-e67621089154.mp4" + }, + { + "id": "0dfdb33b-7ad6-4f49-a58d-e3c4077c98ce", + "created_date": "2024-07-25 07:29:47.375619", + "last_modified_date": "2024-07-25 07:29:47.375619", + "version": 0, + "url": "https://ge.xhamster.com/videos/teenievision-04-teen-house-xhhSL3W", + "review": 0, + "should_download": 0, + "title": "Teenievision 04: Teenie-Haus | xHamster", + "file_name": "Teenievision 04\uff1a Teenie-Haus [xhhSL3W].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0dfdb33b-7ad6-4f49-a58d-e3c4077c98ce.mp4" + }, + { + "id": "0e16ec3d-e002-4dd5-b0bc-df58fabb8f14", + "created_date": "2024-12-29 23:53:27.911408", + "last_modified_date": "2024-12-29 23:53:27.911408", + "version": 0, + "url": "https://ge.xhamster.com/videos/au-pair-full-hd-movie-xh6jPeA", + "review": 0, + "should_download": 0, + "title": "Au-pair (Full HD-Film) | xHamster", + "file_name": "Au-pair (Full HD-Film) [xh6jPeA].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/0e16ec3d-e002-4dd5-b0bc-df58fabb8f14.mp4" + }, + { + "id": "0e4f8b79-52ca-41a7-a8d5-c7a5100f5823", + "created_date": "2024-07-25 07:29:47.179153", + "last_modified_date": "2024-07-25 07:29:47.179153", + "version": 0, + "url": "https://ge.xhamster.com/videos/fucked-on-the-boat-trip-xhP28wx", + "review": 0, + "should_download": 0, + "title": "Auf der Bootsfahrt gefickt | xHamster", + "file_name": "Auf der Bootsfahrt gefickt [xhP28wx].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0e4f8b79-52ca-41a7-a8d5-c7a5100f5823.mp4" + }, + { + "id": "0ea70223-2e5a-4e06-815c-4b9268dbd9ee", + "created_date": "2024-07-25 07:29:46.908038", + "last_modified_date": "2024-07-25 07:29:46.908038", + "version": 0, + "url": "https://ge.xhamster.com/videos/pretty-peaches-ii-1987-9301575", + "review": 0, + "should_download": 0, + "title": "H\u00fcbsche Pfirsiche ii 1987 | xHamster", + "file_name": "H\u00fcbsche Pfirsiche ii 1987 [9301575].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0ea70223-2e5a-4e06-815c-4b9268dbd9ee.mp4" + }, + { + "id": "0ebbb198-c98e-41a6-89a7-0b7234a7cbb4", + "created_date": "2024-07-25 07:29:45.297256", + "last_modified_date": "2024-07-25 07:29:45.297256", + "version": 0, + "url": "https://ge.xhamster.com/videos/18-and-confused-8-xhvgoL0", + "review": 0, + "should_download": 0, + "title": "18 und verwirrt # 8 | xHamster", + "file_name": "18 und verwirrt # 8 [xhvgoL0].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0ebbb198-c98e-41a6-89a7-0b7234a7cbb4.mp4" + }, + { + "id": "0ecb3db6-2aae-4b96-b936-d3d82bc834bb", + "created_date": "2024-07-25 07:29:46.115434", + "last_modified_date": "2024-07-25 07:29:46.115434", + "version": 0, + "url": "https://ge.xhamster.com/videos/teen-babe-gets-fucked-by-teacher-on-his-desk-xhaCa4E", + "review": 0, + "should_download": 0, + "title": "Teen-Sch\u00e4tzchen wird vom Lehrer auf seinem Schreibtisch gefickt | xHamster", + "file_name": "Teen-Sch\u00e4tzchen wird vom Lehrer auf seinem Schreibtisch gefickt [xhaCa4E].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0ecb3db6-2aae-4b96-b936-d3d82bc834bb.mp4" + }, + { + "id": "0f4a7acf-9d51-4159-a488-98f302d0f3e2", + "created_date": "2024-07-25 07:29:44.929141", + "last_modified_date": "2024-07-25 07:29:44.929141", + "version": 0, + "url": "https://ge.xhamster.com/videos/die-schone-und-das-biest-full-movie-xhNN3mC", + "review": 0, + "should_download": 0, + "title": "Die Schone Und Das Biest Full Movie, Free Porn 55 | xHamster", + "file_name": "Die Schone und das Biest (Full Movie) [xhNN3mC].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0f4a7acf-9d51-4159-a488-98f302d0f3e2.mp4" + }, + { + "id": "0f6d3ae5-9656-4087-9a41-00ddf6a35f9c", + "created_date": "2024-08-28 23:21:54.349338", + "last_modified_date": "2024-10-21 16:25:19.155000", + "version": 1, + "url": "https://ge.xhamster.com/videos/bare-family-xhh6JGc", + "review": 0, + "should_download": 0, + "title": "Nackte Familie !! | xHamster", + "file_name": "Nackte Familie !! [xhh6JGc].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/0f6d3ae5-9656-4087-9a41-00ddf6a35f9c.mp4" + }, + { + "id": "0f8967ca-ec35-45eb-8d0f-79b446def076", + "created_date": "2024-09-24 08:11:38.995251", + "last_modified_date": "2024-10-21 16:25:27.755000", + "version": 1, + "url": "https://ge.xhamster.com/videos/adult-time-stepmom-reagan-foxx-caught-her-stepson-fucking-their-milf-neighbor-and-joined-in-xh0ZgLR", + "review": 0, + "should_download": 0, + "title": "Erwachsenenzeit - Stiefmutter Reagan Foxx erwischte ihren Stiefsohn beim Ficken ihrer MILF-Nachbarin und machte mit! | xHamster", + "file_name": "Erwachsenenzeit - Stiefmutter Reagan Foxx erwischte ihren Stiefsohn beim Ficken ihrer MILF-Nachbarin und machte mit! [xh0ZgLR].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/0f8967ca-ec35-45eb-8d0f-79b446def076.mp4" + }, + { + "id": "0f8c2080-850a-44f6-bcee-a30c0454f1c8", + "created_date": "2024-07-25 07:29:44.613623", + "last_modified_date": "2024-12-30 23:04:06.578000", + "version": 1, + "url": "https://ge.xhamster.com/videos/sex-on-a-boat-10541208", + "review": 0, + "should_download": 0, + "title": "Sex auf einem Boot | xHamster", + "file_name": "Sex auf einem Boot [10541208].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/0f8c2080-850a-44f6-bcee-a30c0454f1c8.mp4" + }, + { + "id": "0fb4bd60-6fc6-48e7-a9ce-8b93345c29cc", + "created_date": "2024-07-25 07:29:44.440259", + "last_modified_date": "2024-07-25 07:29:44.440259", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-sex-xhX5gLo", + "review": 0, + "should_download": 0, + "title": "Familiensex | xHamster", + "file_name": "Familiensex [xhX5gLo].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/0fb4bd60-6fc6-48e7-a9ce-8b93345c29cc.mp4" + }, + { + "id": "10732dee-e916-4d7d-8bba-1f651da791e2", + "created_date": "2024-07-25 07:29:44.815651", + "last_modified_date": "2024-07-25 07:29:44.815651", + "version": 0, + "url": "https://ge.xhamster.com/videos/german-2-complete-film-b-r-12714004", + "review": 0, + "should_download": 0, + "title": "Deutsch # 2 - kompletter Film -b $ r | xHamster", + "file_name": "Deutsch # 2 - kompletter Film -b $ r [12714004].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/10732dee-e916-4d7d-8bba-1f651da791e2.mp4" + }, + { + "id": "108ba31b-2d1a-4f85-89c7-745f49ac1e0b", + "created_date": "2024-11-01 21:08:26.928470", + "last_modified_date": "2024-11-01 21:08:26.928470", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-redhead-nurse-loves-threesomes-and-dp-xh6PvRA", + "review": 0, + "should_download": 0, + "title": "Die rothaarige Krankenschwester liebt Dreier und Doppelpenetration | xHamster", + "file_name": "Die rothaarige Krankenschwester liebt Dreier und Doppelpenetration [xh6PvRA].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/108ba31b-2d1a-4f85-89c7-745f49ac1e0b.mp4" + }, + { + "id": "10d7db9b-92ef-48b0-8e9f-64bc3bb13bff", + "created_date": "2024-07-25 07:29:45.340635", + "last_modified_date": "2024-07-25 07:29:45.340635", + "version": 0, + "url": "https://ge.xhamster.com/videos/happy-family-350658", + "review": 0, + "should_download": 0, + "title": "Gl\u00fcckliche Familie | xHamster", + "file_name": "Gl\u00fcckliche Familie [350658].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/10d7db9b-92ef-48b0-8e9f-64bc3bb13bff.mp4" + }, + { + "id": "110694cc-38dc-46a0-8261-9f42b0aefc2f", + "created_date": "2024-07-25 07:29:46.245856", + "last_modified_date": "2024-07-25 07:29:46.245856", + "version": 0, + "url": "https://ge.xhamster.com/videos/fuck-turkey-stepbro-i-want-this-thanksgiving-dick-thinks-demi-hawks-xhDzbsd", + "review": 0, + "should_download": 0, + "title": "\"Fick Truthahn, Stiefbruder! Ich will diesen Thanksgiving-Schwanz!\" denkt, Demi-Falken | xHamster", + "file_name": "\uff02Fick Truthahn, Stiefbruder! Ich will diesen Thanksgiving-Schwanz!\uff02 denkt, Demi-Falken [xhDzbsd].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/110694cc-38dc-46a0-8261-9f42b0aefc2f.mp4" + }, + { + "id": "1147998e-a16f-4b39-b797-480f224cccf0", + "created_date": "2024-07-25 07:29:45.422771", + "last_modified_date": "2024-07-25 07:29:45.422771", + "version": 0, + "url": "https://ge.xhamster.com/videos/are-you-seriously-watching-stepsister-porn-again-mae-milano-asks-her-stepbro-s25-e8-xhD7wCB", + "review": 0, + "should_download": 0, + "title": "\"Siehst du dich ernsthaft wieder Stiefschwester-Porno an?\" Mae Milano fragt ihren Stiefbruder 25: e8 | xHamster", + "file_name": "\uff02Siehst du dich ernsthaft wieder Stiefschwester-Porno an\uff1f\uff02 Mae Milano fragt ihren Stiefbruder 25\uff1a e8 [xhD7wCB].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1147998e-a16f-4b39-b797-480f224cccf0.mp4" + }, + { + "id": "1167fed2-b9f5-42d0-9e6a-5cc11223d233", + "created_date": "2024-07-25 07:29:45.986060", + "last_modified_date": "2024-07-25 07:29:45.986060", + "version": 0, + "url": "https://ge.xhamster.com/videos/hard-threesome-and-dp-on-a-yacht-xhlshBf", + "review": 0, + "should_download": 0, + "title": "Harter Dreier und Doppelpenetration auf einer Yacht | xHamster", + "file_name": "Harter Dreier und Doppelpenetration auf einer Yacht [xhlshBf].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1167fed2-b9f5-42d0-9e6a-5cc11223d233.mp4" + }, + { + "id": "11803550-3c87-418e-8792-f9e60f86f6f1", + "created_date": "2024-07-25 07:29:45.904084", + "last_modified_date": "2024-07-25 07:29:45.904084", + "version": 0, + "url": "https://ge.xhamster.com/videos/kinky-cuisine-3226579", + "review": 0, + "should_download": 0, + "title": "Versaute K\u00fcche | xHamster", + "file_name": "Versaute K\u00fcche [3226579].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/11803550-3c87-418e-8792-f9e60f86f6f1.mp4" + }, + { + "id": "11c068aa-b62b-41b6-b788-e88a5d684447", + "created_date": "2024-09-24 08:11:39.000319", + "last_modified_date": "2024-10-21 16:25:32.881000", + "version": 1, + "url": "https://ge.xhamster.com/videos/freeuse-redhead-step-mother-will-do-anything-to-keep-her-lovely-stepsons-from-joining-the-army-xhYCE7l", + "review": 0, + "should_download": 0, + "title": "Freeuse, rothaarige Stiefmutter wird alles tun, um ihre sch\u00f6nen Stiefs\u00f6hne davon abzuhalten, zur Armee zu gehen | xHamster", + "file_name": "Freeuse, rothaarige Stiefmutter wird alles tun, um ihre sch\u00f6nen Stiefs\u00f6hne davon abzuhalten, zur Armee zu gehen [xhYCE7l].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/11c068aa-b62b-41b6-b788-e88a5d684447.mp4" + }, + { + "id": "11d28038-403f-4e10-b7ac-e6e06bd7405e", + "created_date": "2024-07-25 07:29:46.428543", + "last_modified_date": "2024-07-25 07:29:46.428543", + "version": 0, + "url": "https://ge.xhamster.com/videos/not-so-innocent-step-sister-s15-e4-xhqceki", + "review": 0, + "should_download": 0, + "title": "Nicht so unschuldige stiefschwester - s15: e4 | xHamster", + "file_name": "Nicht so unschuldige stiefschwester - s15\uff1a e4 [xhqceki].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/11d28038-403f-4e10-b7ac-e6e06bd7405e.mp4" + }, + { + "id": "1236448b-7566-4e0a-a3f5-673afa78a69d", + "created_date": "2024-07-25 07:29:46.177166", + "last_modified_date": "2024-07-25 07:29:46.177166", + "version": 0, + "url": "https://ge.xhamster.com/videos/versaute-sex-parties-2002-german-tyra-misoux-full-dvd-xh6i4d8", + "review": 0, + "should_download": 0, + "title": "Versaute Sex-Parties (2002, deutsch, Tyra Misoux, ganze DVD) | xHamster", + "file_name": "Versaute Sex-Parties (2002, deutsch, Tyra Misoux, ganze DVD) [xh6i4d8].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1236448b-7566-4e0a-a3f5-673afa78a69d.mp4" + }, + { + "id": "12dfd3e2-b308-4c66-91a7-7d29ed8a551c", + "created_date": "2024-09-24 08:11:38.998520", + "last_modified_date": "2024-10-21 16:25:38.370000", + "version": 1, + "url": "https://ge.xhamster.com/videos/when-things-go-too-far-xhA25g5", + "review": 0, + "should_download": 0, + "title": "Wenn es zu weit geht | xHamster", + "file_name": "Wenn es zu weit geht [xhA25g5].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/12dfd3e2-b308-4c66-91a7-7d29ed8a551c.mp4" + }, + { + "id": "12e016d5-5fc3-4f06-a027-68d14e338494", + "created_date": "2024-10-14 20:33:38.254151", + "last_modified_date": "2024-10-21 16:25:44.353000", + "version": 1, + "url": "https://ge.xhamster.com/videos/orgy-and-anal-sex-with-gorgeous-babes-xhHvIqI", + "review": 0, + "should_download": 0, + "title": "Orgie und Analsex mit wundersch\u00f6nen Sch\u00e4tzchen | xHamster", + "file_name": "Orgie und Analsex mit wundersch\u00f6nen Sch\u00e4tzchen [xhHvIqI].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/12e016d5-5fc3-4f06-a027-68d14e338494.mp4" + }, + { + "id": "1343795d-66eb-4df6-bf29-78291a26a541", + "created_date": "2024-07-25 07:29:44.666170", + "last_modified_date": "2024-07-25 07:29:44.666170", + "version": 0, + "url": "https://ge.xhamster.com/videos/blonde-step-sister-lena-reif-fucks-arrogant-brother-10639494", + "review": 0, + "should_download": 0, + "title": "Blonde Stiefschwester Lena Reif fickt arroganten Bruder | xHamster", + "file_name": "Blonde Stiefschwester Lena Reif fickt arroganten Bruder [10639494].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1343795d-66eb-4df6-bf29-78291a26a541.mp4" + }, + { + "id": "137eb9f8-49a1-44ac-8a79-d0def4fdae2e", + "created_date": "2024-07-25 07:29:46.332199", + "last_modified_date": "2024-07-25 07:29:46.332199", + "version": 0, + "url": "https://ge.xhamster.com/videos/nuru-massage-busty-masseuse-chanel-preston-bangs-the-stacked-shy-bunny-colby-with-her-boyfriend-xhGw2pz", + "review": 0, + "should_download": 0, + "title": "Nuru Massage - die vollbusige Masseuse Chanel Preston knallt das gestapelte sch\u00fcchterne H\u00e4schen Colby mit ihrem Freund | xHamster", + "file_name": "Nuru Massage - die vollbusige Masseuse Chanel Preston knallt das gestapelte sch\u00fcchterne H\u00e4schen Colby mit ihrem Freund [xhGw2pz].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/137eb9f8-49a1-44ac-8a79-d0def4fdae2e.mp4" + }, + { + "id": "13a42fc9-8699-4a60-8f3d-86b4fb810974", + "created_date": "2024-07-25 07:29:45.711901", + "last_modified_date": "2024-07-25 07:29:45.711901", + "version": 0, + "url": "https://ge.xhamster.com/videos/a-group-of-naked-students-are-in-the-pool-hardly-fucking-3389489", + "review": 0, + "should_download": 0, + "title": "Eine Gruppe nackter Studenten fickt im Pool kaum | xHamster", + "file_name": "Eine Gruppe nackter Studenten fickt im Pool kaum [3389489].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/13a42fc9-8699-4a60-8f3d-86b4fb810974.mp4" + }, + { + "id": "13ca65c2-5501-488f-aca6-8da54fdce0b8", + "created_date": "2024-07-25 07:29:44.269717", + "last_modified_date": "2024-07-25 07:29:44.269717", + "version": 0, + "url": "https://ge.xhamster.com/videos/step-sister-shares-brothers-bed-family-therapy-xhJoDYL", + "review": 0, + "should_download": 0, + "title": "Stiefschwester teilt das Bett des Bruders - Familientherapie | xHamster", + "file_name": "Stiefschwester teilt das Bett des Bruders - Familientherapie [xhJoDYL].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/13ca65c2-5501-488f-aca6-8da54fdce0b8.mp4" + }, + { + "id": "13d82dc3-c7ea-461a-8b19-fcb98f81ff55", + "created_date": "2024-07-25 07:29:47.198552", + "last_modified_date": "2024-07-25 07:29:47.198552", + "version": 0, + "url": "https://ge.xhamster.com/videos/two-busty-german-maids-pleasing-a-hard-pecker-with-their-tight-holes-xhB9aOa", + "review": 0, + "should_download": 0, + "title": "Zwei vollbusige deutsche Hausm\u00e4dchen befriedigen einen harten Schwanz mit ihren engen L\u00f6chern | xHamster", + "file_name": "Zwei vollbusige deutsche Hausm\u00e4dchen befriedigen einen harten Schwanz mit ihren engen L\u00f6chern [xhB9aOa].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/13d82dc3-c7ea-461a-8b19-fcb98f81ff55.mp4" + }, + { + "id": "1408c1b9-7f67-45dd-812c-b4b4d3216f38", + "created_date": "2024-07-25 07:29:47.210281", + "last_modified_date": "2024-07-25 07:29:47.210281", + "version": 0, + "url": "https://ge.xhamster.com/videos/schoolgirl-eduvation-ccc-german-dub-12366609", + "review": 0, + "should_download": 0, + "title": "Schulm\u00e4dchen Eduvation - ccc german dub | xHamster", + "file_name": "Schulm\u00e4dchen Eduvation - ccc german dub [12366609].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1408c1b9-7f67-45dd-812c-b4b4d3216f38.mp4" + }, + { + "id": "147fcbe6-a484-4782-9641-10ba0d77a437", + "created_date": "2024-07-25 07:29:48.041213", + "last_modified_date": "2024-07-25 07:29:48.041213", + "version": 0, + "url": "https://ge.xhamster.com/videos/free-and-perverse-14904792", + "review": 0, + "should_download": 0, + "title": "Frei und pervers | xHamster", + "file_name": "Frei und pervers [14904792].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/147fcbe6-a484-4782-9641-10ba0d77a437.mp4" + }, + { + "id": "14c86b57-e955-4b35-b251-0f2a61962f2a", + "created_date": "2024-07-25 07:29:46.077512", + "last_modified_date": "2024-07-25 07:29:46.077512", + "version": 0, + "url": "https://ge.xhamster.com/videos/students-party-one-girl-3-guys-2206707", + "review": 0, + "should_download": 0, + "title": "Studenten feiern ein M\u00e4dchen 3 Typen | xHamster", + "file_name": "Studenten feiern ein M\u00e4dchen 3 Typen [2206707].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/14c86b57-e955-4b35-b251-0f2a61962f2a.mp4" + }, + { + "id": "14d871a2-bd92-4e0d-91af-e6150e3e2156", + "created_date": "2024-07-25 07:29:45.722334", + "last_modified_date": "2024-07-25 07:29:45.722334", + "version": 0, + "url": "https://ge.xhamster.com/videos/nasse-nymphen-2331575", + "review": 0, + "should_download": 0, + "title": "Nasse Nymphen: Free Anal Porn Video 5c | xHamster", + "file_name": "Nasse Nymphen [2331575].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/14d871a2-bd92-4e0d-91af-e6150e3e2156.mp4" + }, + { + "id": "151dfaa8-d1f5-4fac-9864-0b007ecc6059", + "created_date": "2024-07-25 07:29:47.059735", + "last_modified_date": "2024-07-25 07:29:47.059735", + "version": 0, + "url": "https://ge.xhamster.com/videos/teen-students-dickriding-after-oral-pleasing-12427252", + "review": 0, + "should_download": 0, + "title": "Teen Teen Studenten Schwanz reiten nach oraler Befriedigung | xHamster", + "file_name": "Teen Teen Studenten Schwanz reiten nach oraler Befriedigung [12427252].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/151dfaa8-d1f5-4fac-9864-0b007ecc6059.mp4" + }, + { + "id": "15439c23-ac16-4739-914b-8fcf5d309762", + "created_date": "2024-07-25 07:29:44.539947", + "last_modified_date": "2024-07-25 07:29:44.539947", + "version": 0, + "url": "https://ge.xhamster.com/videos/cum-in-my-mouth-13446477", + "review": 0, + "should_download": 0, + "title": "Sperma auf der Zunge | xHamster", + "file_name": "Sperma auf der Zunge [13446477].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/15439c23-ac16-4739-914b-8fcf5d309762.mp4" + }, + { + "id": "1594945a-3019-42a2-9093-1368e16693cf", + "created_date": "2024-07-25 07:29:46.374380", + "last_modified_date": "2024-07-25 07:29:46.374380", + "version": 0, + "url": "https://ge.xhamster.com/videos/die-neue-assistentin-auf-dem-billardtisch-eingearbeitet-xhRN4kT", + "review": 0, + "should_download": 0, + "title": "Die Neue Assistentin Auf Dem Billardtisch Eingearbeitet | xHamster", + "file_name": "Die neue Assistentin auf dem Billardtisch eingearbeitet [xhRN4kT].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1594945a-3019-42a2-9093-1368e16693cf.mp4" + }, + { + "id": "15efb478-81d6-4028-80de-2959c44d48b0", + "created_date": "2024-07-25 07:29:44.599317", + "last_modified_date": "2024-07-25 07:29:44.599317", + "version": 0, + "url": "https://ge.xhamster.com/videos/juicy-white-dick-plows-young-cunt-12812057", + "review": 0, + "should_download": 0, + "title": "Saftiger wei\u00dfer Schwanz pfl\u00fcgt junge Fotze | xHamster", + "file_name": "Saftiger wei\u00dfer Schwanz pfl\u00fcgt junge Fotze [12812057].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/15efb478-81d6-4028-80de-2959c44d48b0.mp4" + }, + { + "id": "16317d33-8302-4ac0-84db-09a742466149", + "created_date": "2024-08-16 11:21:33.106000", + "last_modified_date": "2024-10-21 16:25:50.795000", + "version": 1, + "url": "https://ge.xhamster.com/videos/how-to-ace-the-job-interview-at-the-free-use-office-feat-vanna-bardot-millie-morgan-freeuse-milf-xh0EtlP", + "review": 0, + "should_download": 0, + "title": "Wie man das Vorstellungsgespr\u00e4ch bei der kostenlosen B\u00fcronutzung acesiert. Vanna bardot & millie morgan - freie milf | xHamster", + "file_name": "Wie man das Vorstellungsgespr\u00e4ch bei der kostenlosen B\u00fcronutzung acesiert. Vanna bardot & millie morgan - freie milf [xh0EtlP].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/16317d33-8302-4ac0-84db-09a742466149.mp4" + }, + { + "id": "16b85580-9de3-4756-b3cd-ed4976d1987f", + "created_date": "2024-07-25 07:29:48.078642", + "last_modified_date": "2024-07-25 07:29:48.078642", + "version": 0, + "url": "https://ge.xhamster.com/videos/brother-fucked-stepsisters-on-family-vacation-xhrUAe6", + "review": 0, + "should_download": 0, + "title": "Bruder fickte Stiefschwestern im Familienurlaub | xHamster", + "file_name": "Bruder fickte Stiefschwestern im Familienurlaub [xhrUAe6].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/16b85580-9de3-4756-b3cd-ed4976d1987f.mp4" + }, + { + "id": "16e42995-8665-4fbf-8726-4d6f9484bb25", + "created_date": "2024-07-25 07:29:47.704233", + "last_modified_date": "2024-07-25 07:29:47.704233", + "version": 0, + "url": "https://ge.xhamster.com/videos/swap-bro-says-i-cant-stop-thinking-about-fucking-you-both-xhLFBE5", + "review": 0, + "should_download": 0, + "title": "Swap-Bro, sagt, ich kann nicht aufh\u00f6ren, dar\u00fcber nachzudenken, dich beide zu ficken | xHamster", + "file_name": "Swap-Bro, sagt, ich kann nicht aufh\u00f6ren, dar\u00fcber nachzudenken, dich beide zu ficken [xhLFBE5].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/16e42995-8665-4fbf-8726-4d6f9484bb25.mp4" + }, + { + "id": "17043616-6ae0-41d7-a240-03f85bbb8581", + "created_date": "2024-07-25 07:29:44.828601", + "last_modified_date": "2024-07-25 07:29:44.828601", + "version": 0, + "url": "https://ge.xhamster.com/videos/hes-really-hot-i-bet-he-has-a-big-dick-xhGfU90", + "review": 0, + "should_download": 0, + "title": "Er ist wirklich hei\u00df, ich wette, er hat einen gro\u00dfen Schwanz! | xHamster", + "file_name": "Er ist wirklich hei\u00df, ich wette, er hat einen gro\u00dfen Schwanz! [xhGfU90].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/17043616-6ae0-41d7-a240-03f85bbb8581.mp4" + }, + { + "id": "173f7ca2-11df-402b-a6be-185c20c7503e", + "created_date": "2024-07-25 07:29:46.122714", + "last_modified_date": "2024-07-25 07:29:46.122714", + "version": 0, + "url": "https://ge.xhamster.com/videos/nude-beach-orgy-5234700", + "review": 0, + "should_download": 0, + "title": "FKK-Strand-Orgie | xHamster", + "file_name": "FKK-Strand-Orgie [5234700].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/173f7ca2-11df-402b-a6be-185c20c7503e.mp4" + }, + { + "id": "176c333c-a9c3-48a9-b7b4-83cc2a3b5fe5", + "created_date": "2024-07-25 07:29:47.386625", + "last_modified_date": "2024-07-25 07:29:47.386625", + "version": 0, + "url": "https://ge.xhamster.com/videos/surprise-gangbang-meridian-14004716", + "review": 0, + "should_download": 0, + "title": "\u00dcberraschungs-Gangbang Meridian | xHamster", + "file_name": "\u00dcberraschungs-Gangbang Meridian [14004716].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/176c333c-a9c3-48a9-b7b4-83cc2a3b5fe5.mp4" + }, + { + "id": "17c37cf5-6d6d-41a6-9d53-1d1b0f460d74", + "created_date": "2024-07-25 07:29:46.780061", + "last_modified_date": "2024-07-25 07:29:46.780061", + "version": 0, + "url": "https://ge.xhamster.com/videos/gang-bang-my-wife-scene-12-xhGnY6Q", + "review": 0, + "should_download": 0, + "title": "Gangbang mit meiner Ehefrau - Szene # 12 | xHamster", + "file_name": "Gangbang mit meiner Ehefrau - Szene # 12 [xhGnY6Q].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/17c37cf5-6d6d-41a6-9d53-1d1b0f460d74.mp4" + }, + { + "id": "17d51cb3-dbce-4879-ae7a-4276ec1cadc3", + "created_date": "2024-07-25 07:29:47.510728", + "last_modified_date": "2024-07-25 07:29:47.510728", + "version": 0, + "url": "https://ge.xhamster.com/videos/tushy-jessa-rhodes-craves-two-cocks-in-amazing-dp-sex-11034717", + "review": 0, + "should_download": 0, + "title": "Tushy Jessa Rhodes sehnt sich nach zwei Schw\u00e4nzen in erstaunlichem Doppelpenetrations-Sex | xHamster", + "file_name": "Tushy Jessa Rhodes sehnt sich nach zwei Schw\u00e4nzen in erstaunlichem Doppelpenetrations-Sex [11034717].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/17d51cb3-dbce-4879-ae7a-4276ec1cadc3.mp4" + }, + { + "id": "184b0462-705b-497d-8f32-2936492f2260", + "created_date": "2024-07-25 07:29:47.755820", + "last_modified_date": "2024-07-25 07:29:47.755820", + "version": 0, + "url": "https://ge.xhamster.com/videos/couple-gets-horny-female-massage-therapist-xh6xaSe", + "review": 0, + "should_download": 0, + "title": "Paar bekommt geile weibliche Masseurin | xHamster", + "file_name": "Paar bekommt geile weibliche Masseurin [xh6xaSe].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/184b0462-705b-497d-8f32-2936492f2260.mp4" + }, + { + "id": "185b5318-238b-426b-b4f4-f617a9763eb2", + "created_date": "2024-07-25 07:29:47.464957", + "last_modified_date": "2024-07-25 07:29:47.464957", + "version": 0, + "url": "https://ge.xhamster.com/videos/a-group-of-college-teenagers-playing-funny-sex-games-1409432", + "review": 0, + "should_download": 0, + "title": "Eine Gruppe von College-Teenagern, die lustige Sexspiele spielen | xHamster", + "file_name": "Eine Gruppe von College-Teenagern, die lustige Sexspiele spielen [1409432].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/185b5318-238b-426b-b4f4-f617a9763eb2.mp4" + }, + { + "id": "18677cf7-29cf-4dc5-a7e3-f6d6174670d0", + "created_date": "2024-07-25 07:29:46.760907", + "last_modified_date": "2024-07-25 07:29:46.760907", + "version": 0, + "url": "https://ge.xhamster.com/videos/darling-assfucked-in-the-vineyard-13623559", + "review": 0, + "should_download": 0, + "title": "Jane Darling wird im Weinberg arschgefickt | xHamster", + "file_name": "Jane Darling wird im Weinberg arschgefickt [13623559].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/18677cf7-29cf-4dc5-a7e3-f6d6174670d0.mp4" + }, + { + "id": "1886466c-7006-4482-a62c-75ff0bffea62", + "created_date": "2024-07-25 07:29:45.225976", + "last_modified_date": "2024-07-25 07:29:45.225976", + "version": 0, + "url": "https://ge.xhamster.com/videos/a-family-evening-xhryxr8", + "review": 0, + "should_download": 0, + "title": "Ein Familienabend | xHamster", + "file_name": "Ein Familienabend [xhryxr8].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1886466c-7006-4482-a62c-75ff0bffea62.mp4" + }, + { + "id": "1889d936-679d-4a06-89a2-40a4e24325a3", + "created_date": "2024-07-25 07:29:44.338453", + "last_modified_date": "2024-07-25 07:29:44.338453", + "version": 0, + "url": "https://ge.xhamster.com/videos/decided-to-walk-naked-into-his-stepsisters-room-xhOCdLI", + "review": 0, + "should_download": 0, + "title": "Beschloss, nackt in das zimmer seiner stiefschwester zu gehen | xHamster", + "file_name": "Beschloss, nackt in das zimmer seiner stiefschwester zu gehen [xhOCdLI].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1889d936-679d-4a06-89a2-40a4e24325a3.mp4" + }, + { + "id": "18a2fb00-14fc-4667-b777-cdd4af361e2c", + "created_date": "2024-08-28 23:21:54.364504", + "last_modified_date": "2024-08-28 23:21:54.364504", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-hubby-fucks-your-wife-in-front-of-us-xh8sNxF", + "review": 0, + "should_download": 0, + "title": "Mein ehemann fickt deine ehefrau vor uns | xHamster", + "file_name": "Mein ehemann fickt deine ehefrau vor uns [xh8sNxF].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/18a2fb00-14fc-4667-b777-cdd4af361e2c.mp4" + }, + { + "id": "18d70c6f-89e5-413f-b300-f04aa8737dc0", + "created_date": "2024-07-25 07:29:47.801289", + "last_modified_date": "2024-07-25 07:29:47.801289", + "version": 0, + "url": "https://ge.xhamster.com/videos/wowgirls-michelle-red-fox-and-the-lucky-guy-in-a-hot-fuck-scene-xh3gx6r", + "review": 0, + "should_download": 0, + "title": "Wowgirls - Michelle Red Fox und der Gl\u00fcckspilz in einer hei\u00dfen Fickszene | xHamster", + "file_name": "Wowgirls - Michelle Red Fox und der Gl\u00fcckspilz in einer hei\u00dfen Fickszene [xh3gx6r].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/18d70c6f-89e5-413f-b300-f04aa8737dc0.mp4" + }, + { + "id": "18ecad86-c84f-4315-90c4-da14613a35ac", + "created_date": "2024-07-25 07:29:46.420575", + "last_modified_date": "2024-07-25 07:29:46.420575", + "version": 0, + "url": "https://ge.xhamster.com/videos/wild-and-crazy-girlfriend-vacation-14527087", + "review": 0, + "should_download": 0, + "title": "Wilder und verr\u00fcckter Urlaub mit Freundin | xHamster", + "file_name": "Wilder und verr\u00fcckter Urlaub mit Freundin [14527087].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/18ecad86-c84f-4315-90c4-da14613a35ac.mp4" + }, + { + "id": "18faa3f3-b87b-4400-bd2b-5b24ad1ba386", + "created_date": "2024-07-25 07:29:45.355684", + "last_modified_date": "2024-07-25 07:29:45.355684", + "version": 0, + "url": "https://ge.xhamster.com/videos/madchen-internat-2-xhasv9y", + "review": 0, + "should_download": 0, + "title": "Madchen Internat 2: Free Porn Video 93 | xHamster", + "file_name": "Madchen internat 2 [xhasv9y].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/18faa3f3-b87b-4400-bd2b-5b24ad1ba386.mp4" + }, + { + "id": "1902ad09-cf29-489e-963c-821a09d8cb68", + "created_date": "2024-07-25 07:29:44.286339", + "last_modified_date": "2024-07-25 07:29:44.286339", + "version": 0, + "url": "https://ge.xhamster.com/videos/step-daughter-catches-scandalous-step-mom-seducing-her-weak-boyfriend-xh2I9PB", + "review": 0, + "should_download": 0, + "title": "Stieftochter erwischt skandal\u00f6se Stiefmutter, die ihren schwachen Freund verf\u00fchrt | xHamster", + "file_name": "Stieftochter erwischt skandal\u00f6se Stiefmutter, die ihren schwachen Freund verf\u00fchrt [xh2I9PB].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1902ad09-cf29-489e-963c-821a09d8cb68.mp4" + }, + { + "id": "195e7c52-43b2-42ed-9475-a144f7e8d072", + "created_date": "2024-11-10 16:53:33.465101", + "last_modified_date": "2024-11-10 16:53:33.465101", + "version": 0, + "url": "https://ge.xhamster.com/videos/a-hot-unfaithful-wife-full-movie-xhcJZSS", + "review": 0, + "should_download": 0, + "title": "Eine hei\u00dfe untreue Ehefrau! (kompletter Film) | xHamster", + "file_name": "Eine hei\u00dfe untreue Ehefrau! (kompletter Film) [xhcJZSS].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/195e7c52-43b2-42ed-9475-a144f7e8d072.mp4" + }, + { + "id": "19610b21-0900-4abc-8b67-54ca043a55e7", + "created_date": "2024-07-25 07:29:44.432278", + "last_modified_date": "2024-07-25 07:29:44.432278", + "version": 0, + "url": "https://ge.xhamster.com/videos/blondine-zum-fremdfick-ueberredet-xh4ektS", + "review": 0, + "should_download": 0, + "title": "Blondine Zum Fremdfick Ueberredet, Free Porn 78 | xHamster", + "file_name": "Blondine zum fremdfick ueberredet [xh4ektS].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/19610b21-0900-4abc-8b67-54ca043a55e7.mp4" + }, + { + "id": "196e5626-0dea-431e-b628-a4efdc7cbf02", + "created_date": "2024-08-16 11:11:33.384000", + "last_modified_date": "2024-10-21 16:25:59.964000", + "version": 1, + "url": "https://ge.xhamster.com/videos/sturmfreie-bude-full-movie-xhQA3jC", + "review": 0, + "should_download": 0, + "title": "Sturmfreie Bude Full Movie, Free German Porn 49 | xHamster", + "file_name": "Sturmfreie Bude (Full Movie) [xhQA3jC].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/196e5626-0dea-431e-b628-a4efdc7cbf02.mp4" + }, + { + "id": "1a45e084-c4af-4c9f-877c-e4cddd87e4a3", + "created_date": "2024-07-25 07:29:45.643475", + "last_modified_date": "2024-07-25 07:29:45.643475", + "version": 0, + "url": "https://ge.xhamster.com/videos/fucking-stepsister-and-her-friend-xhfoMPh", + "review": 0, + "should_download": 0, + "title": "Stiefschwester und ihre Freundin gefickt | xHamster", + "file_name": "Stiefschwester und ihre Freundin gefickt [xhfoMPh].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1a45e084-c4af-4c9f-877c-e4cddd87e4a3.mp4" + }, + { + "id": "1a4efc43-b6d8-49ef-818c-2eb0926fa97a", + "created_date": "2024-10-21 15:08:43.554935", + "last_modified_date": "2024-10-21 16:26:05.537000", + "version": 1, + "url": "https://ge.xhamster.com/videos/beautiful-wife-shared-with-a-perfect-stranger-she-just-met-xhSPAr0", + "review": 0, + "should_download": 0, + "title": "Sch\u00f6ne Ehefrau mit einem perfekten Fremden geteilt, den sie gerade getroffen hat | xHamster", + "file_name": "Sch\u00f6ne Ehefrau mit einem perfekten Fremden geteilt, den sie gerade getroffen hat [xhSPAr0].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/1a4efc43-b6d8-49ef-818c-2eb0926fa97a.mp4" + }, + { + "id": "1a6b8e5e-2750-4bfb-9796-5a087a760171", + "created_date": "2025-01-16 19:59:59.779771", + "last_modified_date": "2025-01-16 19:59:59.779777", + "version": 0, + "url": "https://ge.xhamster.com/videos/horny-stepdaughter-myra-moans-tries-new-clothes-and-rough-foursome-with-hot-stepmom-jessica-ryan-xhWJ33m", + "review": 0, + "should_download": 0, + "title": "Die geile stieftochter Myra Moans probiert neue kleider und groben vierer mit der hei\u00dfen stiefmutter Jessica Ryan | xHamster", + "file_name": "Die geile stieftochter Myra Moans probiert neue kleider und groben vierer mit der hei\u00dfen stiefmutter Jessica Ryan [xhWJ33m].mp4", + "path": null, + "cloud_link": "/data/media/1a6b8e5e-2750-4bfb-9796-5a087a760171.mp4" + }, + { + "id": "1a9db7f8-b0cb-4aca-9bcb-13948359415f", + "created_date": "2025-01-03 00:23:56.611000", + "last_modified_date": "2025-01-03 01:48:13.298000", + "version": 3, + "url": "https://ge.xhamster.com/videos/sechs-schwedinnen-von-der-tankstelle-xhOSyVS", + "review": 0, + "should_download": 0, + "title": "Sechs Schwedinnen Von Der Tankstelle, HD Porn c4 | xHamster", + "file_name": "Sechs Schwedinnen Von Der Tankstelle [xhOSyVS].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/1a9db7f8-b0cb-4aca-9bcb-13948359415f.mp4" + }, + { + "id": "1ab8adcd-7a1b-47cd-91d4-66b48dbc03d9", + "created_date": "2024-07-25 07:29:45.650368", + "last_modified_date": "2024-07-25 07:29:45.650368", + "version": 0, + "url": "https://ge.xhamster.com/videos/trio-anal-10472743", + "review": 0, + "should_download": 0, + "title": "Trio anal | xHamster", + "file_name": "Trio anal [10472743].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1ab8adcd-7a1b-47cd-91d4-66b48dbc03d9.mp4" + }, + { + "id": "1ae765f7-bdf0-48c0-9697-f28194efdc34", + "created_date": "2024-07-25 07:29:46.757363", + "last_modified_date": "2024-07-25 07:29:46.757363", + "version": 0, + "url": "https://ge.xhamster.com/videos/pool-cleaner-fucks-the-daughter-by-the-owner-12803404", + "review": 0, + "should_download": 0, + "title": "Pool Reiniger fickt die Tochter vom Eigent\u00fcmer | xHamster", + "file_name": "Pool Reiniger fickt die Tochter vom Eigent\u00fcmer [12803404].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1ae765f7-bdf0-48c0-9697-f28194efdc34.mp4" + }, + { + "id": "1afef720-efef-4f72-a196-c9b364321ba2", + "created_date": "2024-07-25 07:29:44.872265", + "last_modified_date": "2024-07-25 07:29:44.872265", + "version": 0, + "url": "https://ge.xhamster.com/videos/silly-games-get-my-step-sister-to-fuck-and-swallow-s6-e5-9033570", + "review": 0, + "should_download": 0, + "title": "Silly Games bringen meine Stiefschwester dazu, s6: e5 zu ficken und zu schlucken | xHamster", + "file_name": "Silly Games bringen meine Stiefschwester dazu, s6\uff1a e5 zu ficken und zu schlucken [9033570].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1afef720-efef-4f72-a196-c9b364321ba2.mp4" + }, + { + "id": "1b04b3a5-200b-45f4-bb40-54d9c850fb8a", + "created_date": "2024-07-25 07:29:45.741757", + "last_modified_date": "2024-07-25 07:29:45.741757", + "version": 0, + "url": "https://ge.xhamster.com/videos/meine-geile-nachbarin-12-9178748", + "review": 0, + "should_download": 0, + "title": "Meine Geile Nachbarin 12, Free German Porn 24 | xHamster", + "file_name": "Meine Geile Nachbarin 12 [9178748].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1b04b3a5-200b-45f4-bb40-54d9c850fb8a.mp4" + }, + { + "id": "1bab7852-6f48-4326-8170-b8aeeedb9c1e", + "created_date": "2024-07-25 07:29:46.736640", + "last_modified_date": "2024-07-25 07:29:46.736640", + "version": 0, + "url": "https://ge.xhamster.com/videos/schulmadchen-sommer-lust-und-bauernlummel-1065773", + "review": 0, + "should_download": 0, + "title": "Schulmadchen - Sommer Lust Und Bauernlummel: Free Porn 10 | xHamster", + "file_name": "Schulmadchen - Sommer, Lust Und Bauernlummel [1065773].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1bab7852-6f48-4326-8170-b8aeeedb9c1e.mp4" + }, + { + "id": "1baf7b64-87a2-4365-8b50-73c63763ec3e", + "created_date": "2024-07-25 07:29:45.308351", + "last_modified_date": "2024-07-25 07:29:45.308351", + "version": 0, + "url": "https://ge.xhamster.com/videos/sommer-des-sex-full-movie-xhbVEU7", + "review": 0, + "should_download": 0, + "title": "Sommer des sex (kompletter Film) | xHamster", + "file_name": "Sommer des sex (kompletter Film) [xhbVEU7].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1baf7b64-87a2-4365-8b50-73c63763ec3e.mp4" + }, + { + "id": "1c35d1db-1ec5-4b49-ab21-2beafa8c3a10", + "created_date": "2024-07-25 07:29:46.858233", + "last_modified_date": "2024-07-25 07:29:46.858233", + "version": 0, + "url": "https://ge.xhamster.com/videos/familienschweinchen-xh1vbrg", + "review": 0, + "should_download": 0, + "title": "Familienschweinchen: Free Porn Video 63 | xHamster", + "file_name": "Familienschweinchen [xh1vbrg].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1c35d1db-1ec5-4b49-ab21-2beafa8c3a10.mp4" + }, + { + "id": "1c434846-349b-47f7-a3e8-884d524c5e4b", + "created_date": "2024-07-25 07:29:47.593156", + "last_modified_date": "2024-07-25 07:29:47.593156", + "version": 0, + "url": "https://ge.xhamster.com/videos/every-boss-needs-an-employee-like-chanel-preston-2189041", + "review": 0, + "should_download": 0, + "title": "Jeder Chef braucht einen Angestellten wie Chanel Preston | xHamster", + "file_name": "Jeder Chef braucht einen Angestellten wie Chanel Preston [2189041].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1c434846-349b-47f7-a3e8-884d524c5e4b.mp4" + }, + { + "id": "1c5944f7-54c3-4384-a0ef-5b248e6f1f93", + "created_date": "2024-07-25 07:29:47.326537", + "last_modified_date": "2024-07-25 07:29:47.326537", + "version": 0, + "url": "https://ge.xhamster.com/videos/party-hardcore-gone-crazy-35-amateur-edit-xhlb8Jp", + "review": 0, + "should_download": 0, + "title": "Party-Hardcore verr\u00fcckt 35 - Amateur-Edit | xHamster", + "file_name": "Party-Hardcore verr\u00fcckt 35 - Amateur-Edit [xhlb8Jp].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1c5944f7-54c3-4384-a0ef-5b248e6f1f93.mp4" + }, + { + "id": "1c634605-d6b7-4c5c-8bd5-59391305ad32", + "created_date": "2024-07-25 07:29:47.268555", + "last_modified_date": "2024-07-25 07:29:47.268555", + "version": 0, + "url": "https://ge.xhamster.com/videos/vixen-sex-with-my-boss-7269259", + "review": 0, + "should_download": 0, + "title": "Vixen - Sex mit meinem Chef | xHamster", + "file_name": "Vixen - Sex mit meinem Chef [7269259].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/1c634605-d6b7-4c5c-8bd5-59391305ad32.mp4" + }, + { + "id": "1c83e2b6-9ee1-499a-b5c9-31da57018f1a", + "created_date": "2024-07-25 07:29:47.589661", + "last_modified_date": "2024-07-25 07:29:47.589661", + "version": 0, + "url": "https://ge.xhamster.com/videos/bockingen-xh9Qxth", + "review": 0, + "should_download": 0, + "title": "Bockingen | xHamster", + "file_name": "Bockingen [xh9Qxth].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1c83e2b6-9ee1-499a-b5c9-31da57018f1a.mp4" + }, + { + "id": "1ca21517-0879-41f4-a40e-8b31fc31444e", + "created_date": "2024-07-25 07:29:44.352116", + "last_modified_date": "2024-07-25 07:29:44.352116", + "version": 0, + "url": "https://ge.xhamster.com/videos/truth-or-dare-1-6186830", + "review": 0, + "should_download": 0, + "title": "Truth or Dare 1: Free Threesome HD Porn Video e7 | xHamster", + "file_name": "Truth or Dare 1 [6186830].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1ca21517-0879-41f4-a40e-8b31fc31444e.mp4" + }, + { + "id": "1ccef46d-730c-44c1-8ac3-360ab8fe9015", + "created_date": "2024-07-25 07:29:47.839401", + "last_modified_date": "2024-07-25 07:29:47.839401", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsis-says-take-off-your-clothes-so-i-can-see-you-naked-xhzOa0C", + "review": 0, + "should_download": 0, + "title": "Stiefschwester sagt, zieh deine Kleidung aus, damit ich dich nackt sehen kann! | xHamster", + "file_name": "Stiefschwester sagt, zieh deine Kleidung aus, damit ich dich nackt sehen kann! [xhzOa0C].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1ccef46d-730c-44c1-8ac3-360ab8fe9015.mp4" + }, + { + "id": "1cf691e5-3fb3-4c83-8aa2-bdfafc62ebb4", + "created_date": "2024-07-25 07:29:47.491314", + "last_modified_date": "2024-07-25 07:29:47.491314", + "version": 0, + "url": "https://ge.xhamster.com/videos/familienskandal-7259344", + "review": 0, + "should_download": 0, + "title": "Familienskandal: Free Anal Porn Video 0c | xHamster", + "file_name": "Familienskandal [7259344].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1cf691e5-3fb3-4c83-8aa2-bdfafc62ebb4.mp4" + }, + { + "id": "1d466252-dae4-461e-b680-dc213e45891a", + "created_date": "2024-07-25 07:29:45.304556", + "last_modified_date": "2024-07-25 07:29:45.304556", + "version": 0, + "url": "https://ge.xhamster.com/videos/you-should-come-lick-it-off-madison-summers-tells-stepbro-s21-e6-xhfCo0X", + "review": 0, + "should_download": 0, + "title": "\"Du solltest kommen und es ablecken\", sagt Madison Summers zum Stiefbruder -s21: e6 | xHamster", + "file_name": "\uff02Du solltest kommen und es ablecken\uff02, sagt Madison Summers zum Stiefbruder -s21\uff1a e6 [xhfCo0X].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1d466252-dae4-461e-b680-dc213e45891a.mp4" + }, + { + "id": "1d62abf0-adc1-4281-a4e5-efc65bd5da10", + "created_date": "2024-08-09 21:17:53.932680", + "last_modified_date": "2024-08-16 10:27:23.346000", + "version": 1, + "url": "https://ge.xhamster.com/videos/wij-12195485", + "review": 0, + "should_download": 0, + "title": "Wij: HD Porn Video 6a | xHamster", + "file_name": "Wij [12195485].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/1d62abf0-adc1-4281-a4e5-efc65bd5da10.mp4" + }, + { + "id": "1d652293-0045-4c03-93f7-a38b38d52890", + "created_date": "2024-07-25 07:29:44.480812", + "last_modified_date": "2024-07-25 07:29:44.480812", + "version": 0, + "url": "https://ge.xhamster.com/videos/secretary-14389288", + "review": 0, + "should_download": 0, + "title": "Sekret\u00e4rin | xHamster", + "file_name": "Sekret\u00e4rin [14389288].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1d652293-0045-4c03-93f7-a38b38d52890.mp4" + }, + { + "id": "1d6c6148-3174-4964-a6b2-5ad929f4fccd", + "created_date": "2024-11-01 21:08:26.935627", + "last_modified_date": "2024-11-01 21:08:26.935627", + "version": 0, + "url": "https://ge.xhamster.com/videos/vintage-70s-die-liebes-insel-02-xhWACK9", + "review": 0, + "should_download": 0, + "title": "Vintage 70s - Die Liebes-insel - 02, Free Porn 34 | xHamster", + "file_name": "vintage 70s - Die Liebes-Insel - 02 [xhWACK9].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/1d6c6148-3174-4964-a6b2-5ad929f4fccd.mp4" + }, + { + "id": "1da53abe-8fa9-4bfd-9942-4a2f7a6b2f8a", + "created_date": "2024-07-25 07:29:45.673619", + "last_modified_date": "2024-07-25 07:29:45.673619", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-strokes-stepsiblings-get-sexual-on-family-vacation-xhBnMSE", + "review": 0, + "should_download": 0, + "title": "In Family Strokes werden Stiefgeschwister im Familienurlaub sexuell | xHamster", + "file_name": "In Family Strokes werden Stiefgeschwister im Familienurlaub sexuell [xhBnMSE].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/1da53abe-8fa9-4bfd-9942-4a2f7a6b2f8a.mp4" + }, + { + "id": "1decc124-b356-47ca-898c-0317bb53f367", + "created_date": "2024-07-25 07:29:46.449719", + "last_modified_date": "2024-07-25 07:29:46.449719", + "version": 0, + "url": "https://ge.xhamster.com/videos/what-would-i-do-to-my-stepbrother-s30-e2-xhqQlGG", + "review": 0, + "should_download": 0, + "title": "Was w\u00fcrde ich mit meinem stiefbruer machen - s30: e2 | xHamster", + "file_name": "Was w\u00fcrde ich mit meinem stiefbruer machen - s30\uff1a e2 [xhqQlGG].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1decc124-b356-47ca-898c-0317bb53f367.mp4" + }, + { + "id": "1e1169b4-1fb5-4993-9edc-cd467bb628f9", + "created_date": "2024-07-25 07:29:45.708198", + "last_modified_date": "2024-07-25 07:29:45.708198", + "version": 0, + "url": "https://ge.xhamster.com/videos/hot-babysitter-dolly-leigh-gets-stuck-and-fucked-xhtx57x", + "review": 0, + "should_download": 0, + "title": "Die hei\u00dfe Babysitterin Dolly Leigh bleibt stecken und wird gefickt | xHamster", + "file_name": "Die hei\u00dfe Babysitterin Dolly Leigh bleibt stecken und wird gefickt [xhtx57x].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/1e1169b4-1fb5-4993-9edc-cd467bb628f9.mp4" + }, + { + "id": "1eb6b9bb-2484-499a-a973-8a01b4bc6c29", + "created_date": "2024-07-25 07:29:48.070939", + "last_modified_date": "2024-07-25 07:29:48.070939", + "version": 0, + "url": "https://ge.xhamster.com/videos/you-should-spend-more-time-outdoors-11-6809804", + "review": 0, + "should_download": 0, + "title": "Sie sollten mehr Zeit im Freien verbringen # 11 | xHamster", + "file_name": "Sie sollten mehr Zeit im Freien verbringen # 11 [6809804].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1eb6b9bb-2484-499a-a973-8a01b4bc6c29.mp4" + }, + { + "id": "1ec67004-532d-4fd0-bdeb-fc7526e9d3f0", + "created_date": "2024-07-25 07:29:44.454997", + "last_modified_date": "2024-07-25 07:29:44.454997", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsis-says-do-you-have-a-hard-on-xhdeAeu", + "review": 0, + "should_download": 0, + "title": "Stiefschwester sagt, hast du es hart? | xHamster", + "file_name": "Stiefschwester sagt, hast du es hart\uff1f [xhdeAeu].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1ec67004-532d-4fd0-bdeb-fc7526e9d3f0.mp4" + }, + { + "id": "1eed7a37-b462-403c-adf6-6120361b0c1b", + "created_date": "2024-09-24 08:11:39.000648", + "last_modified_date": "2024-10-21 16:26:12.693000", + "version": 1, + "url": "https://ge.xhamster.com/videos/4er-sex-loyalty-test-confused-stepbrother-and-husband-of-my-girlfriend-xh3o44U", + "review": 0, + "should_download": 0, + "title": "4er Sex?! TREUETEST!!! Stiefbruder und Ehemann meiner Freundin verwechselt ! | xHamster", + "file_name": "4er Sex\uff1f! TREUETEST!!! Stiefbruder und Ehemann meiner Freundin verwechselt ! [xh3o44U].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/1eed7a37-b462-403c-adf6-6120361b0c1b.mp4" + }, + { + "id": "1f1fd37b-55ec-48df-8971-91039028f1c1", + "created_date": "2024-07-25 07:29:45.937602", + "last_modified_date": "2024-07-25 07:29:45.937602", + "version": 0, + "url": "https://ge.xhamster.com/videos/colonized-4-xhTYd79", + "review": 0, + "should_download": 0, + "title": "Kolonisiert # 4 | xHamster", + "file_name": "Kolonisiert # 4 [xhTYd79].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1f1fd37b-55ec-48df-8971-91039028f1c1.mp4" + }, + { + "id": "1f3b17b6-08de-4ab8-80b1-435eb4524883", + "created_date": "2025-01-16 20:00:03.537011", + "last_modified_date": "2025-01-16 20:49:08.140000", + "version": 2, + "url": "https://ge.xhamster.com/videos/a-night-out-at-the-bar-bold-move-on-sexy-customer-xh0G8BP", + "review": 0, + "should_download": 0, + "title": "Eine nacht an der bar: Mutiger zug auf sexy kunden | xHamster", + "file_name": "Eine nacht an der bar\uff1a Mutiger zug auf sexy kunden [xh0G8BP].mp4", + "path": null, + "cloud_link": "/data/media/1f3b17b6-08de-4ab8-80b1-435eb4524883.mp4" + }, + { + "id": "1f781d06-ee8d-4251-be6a-734fbd837b61", + "created_date": "2024-08-16 11:12:03.493000", + "last_modified_date": "2024-10-21 16:26:18.292000", + "version": 1, + "url": "https://ge.xhamster.com/videos/stacy-open-her-legs-to-seduce-friend-son-near-his-step-mom-xhL1g83", + "review": 0, + "should_download": 0, + "title": "Stacy \u00f6ffnet ihre Beine, um den Sohn ihres Freundes nahe seiner Stiefmutter zu verf\u00fchren | xHamster", + "file_name": "Stacy \u00f6ffnet ihre Beine, um den Sohn ihres Freundes nahe seiner Stiefmutter zu verf\u00fchren [xhL1g83].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/1f781d06-ee8d-4251-be6a-734fbd837b61.mp4" + }, + { + "id": "1fa2a2fe-e1b9-41b7-8063-0ae95d98da01", + "created_date": "2024-07-25 07:29:46.725872", + "last_modified_date": "2024-07-25 07:29:46.725872", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-love-boat-1833824", + "review": 0, + "should_download": 0, + "title": "Das Liebesboot | xHamster", + "file_name": "Das Liebesboot [1833824].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1fa2a2fe-e1b9-41b7-8063-0ae95d98da01.mp4" + }, + { + "id": "1fd3cf92-ab69-4c52-b9fe-fef8c8aec0d9", + "created_date": "2024-07-25 07:29:47.235502", + "last_modified_date": "2024-07-25 07:29:47.235502", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-young-secretaries-1974-7224683", + "review": 0, + "should_download": 0, + "title": "Die jungen Sekret\u00e4rinnen (1974) | xHamster", + "file_name": "Die jungen Sekret\u00e4rinnen (1974) [7224683].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1fd3cf92-ab69-4c52-b9fe-fef8c8aec0d9.mp4" + }, + { + "id": "1fd576af-9d65-4f32-80e9-1b14d931ee41", + "created_date": "2024-07-25 07:29:46.698554", + "last_modified_date": "2024-07-25 07:29:46.698554", + "version": 0, + "url": "https://ge.xhamster.com/videos/gangbang-in-the-rain-233403", + "review": 0, + "should_download": 0, + "title": "Gangbang im Regen | xHamster", + "file_name": "Gangbang im Regen [233403].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1fd576af-9d65-4f32-80e9-1b14d931ee41.mp4" + }, + { + "id": "1fd7c41e-38ca-4a3b-8771-5c78b3768394", + "created_date": "2024-07-25 07:29:44.741121", + "last_modified_date": "2024-07-25 07:29:44.741121", + "version": 0, + "url": "https://ge.xhamster.com/videos/beach-orgy-13242586", + "review": 0, + "should_download": 0, + "title": "Strandorgie | xHamster", + "file_name": "Strandorgie [13242586].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1fd7c41e-38ca-4a3b-8771-5c78b3768394.mp4" + }, + { + "id": "1fe8b8a9-64b5-46a9-a28f-0444c291e26d", + "created_date": "2024-07-25 07:29:45.211396", + "last_modified_date": "2024-07-25 07:29:45.211396", + "version": 0, + "url": "https://ge.xhamster.com/videos/freeuse-fantasy-horny-stepbro-fucks-his-stepsisters-best-friend-hazel-heart-in-front-of-her-xhH3EUp", + "review": 0, + "should_download": 0, + "title": "Freeuse Fantasy, geiler Stiefbruder fickt Hazel Heart, den besten Freund seiner Stiefschwester, vor ihr | xHamster", + "file_name": "Freeuse Fantasy, geiler Stiefbruder fickt Hazel Heart, den besten Freund seiner Stiefschwester, vor ihr [xhH3EUp].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/1fe8b8a9-64b5-46a9-a28f-0444c291e26d.mp4" + }, + { + "id": "201b0ea8-52b7-44c5-83b3-527501bcde81", + "created_date": "2024-07-25 07:29:46.297761", + "last_modified_date": "2024-07-25 07:29:46.297761", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-rent-is-late-but-elles-ass-is-on-time-xhzSJml", + "review": 0, + "should_download": 0, + "title": "Die miete ist sp\u00e4t, aber Elles arsch ist zur zeit | xHamster", + "file_name": "Die miete ist sp\u00e4t, aber Elles arsch ist zur zeit [xhzSJml].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/201b0ea8-52b7-44c5-83b3-527501bcde81.mp4" + }, + { + "id": "2020c00c-bd67-4e1d-9d91-c147d25a5efa", + "created_date": "2024-07-25 07:29:47.892877", + "last_modified_date": "2024-07-25 07:29:47.892877", + "version": 0, + "url": "https://ge.xhamster.com/videos/schoolgirls-8-xhgbBQY", + "review": 0, + "should_download": 0, + "title": "Schulm\u00e4dchen 8 - Blutjunge Sch\u00fclerinnen ... Spritzig Verf\u00fchrt! | xHamster", + "file_name": "Schulm\u00e4dchen 8 - Blutjunge Sch\u00fclerinnen ... Spritzig Verf\u00fchrt! [xhgbBQY].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2020c00c-bd67-4e1d-9d91-c147d25a5efa.mp4" + }, + { + "id": "203a71c8-bdfd-4efe-b4b9-7c9f9428f932", + "created_date": "2024-07-25 07:29:44.902881", + "last_modified_date": "2024-07-25 07:29:44.902881", + "version": 0, + "url": "https://ge.xhamster.com/videos/outdoor-threesome-by-the-lake-8770332", + "review": 0, + "should_download": 0, + "title": "Outdoor-Dreier am See | xHamster", + "file_name": "Outdoor-Dreier am See [8770332].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/203a71c8-bdfd-4efe-b4b9-7c9f9428f932.mp4" + }, + { + "id": "205c35fb-688e-44d4-8550-84e3278d45bb", + "created_date": "2024-07-25 07:29:45.869249", + "last_modified_date": "2024-07-25 07:29:45.869249", + "version": 0, + "url": "https://ge.xhamster.com/videos/rk-prime-abella-danger-sean-lawless-her-ex-fucks-her-11577067", + "review": 0, + "should_download": 0, + "title": "Rk prime - Abella Danger Sean Lawless - ihr Ex fickt sie | xHamster", + "file_name": "Rk prime - Abella Danger Sean Lawless - ihr Ex fickt sie [11577067].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/205c35fb-688e-44d4-8550-84e3278d45bb.mp4" + }, + { + "id": "205e4eb0-a1a7-45f7-ad02-d25a287692c5", + "created_date": "2024-07-25 07:29:47.975713", + "last_modified_date": "2024-07-25 07:29:47.975713", + "version": 0, + "url": "https://ge.xhamster.com/videos/maedel-im-urlaub-beim-sonnenbaden-und-massieren-xhuN7OP", + "review": 0, + "should_download": 0, + "title": "Maedel Im Urlaub Beim Sonnenbaden Und Massieren: HD Porn c7 | xHamster", + "file_name": "Maedel im Urlaub beim sonnenbaden und massieren [xhuN7OP].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/205e4eb0-a1a7-45f7-ad02-d25a287692c5.mp4" + }, + { + "id": "20b95aa4-404b-40f6-bbd2-810d487e88c6", + "created_date": "2024-07-25 07:29:44.536383", + "last_modified_date": "2024-07-25 07:29:44.536383", + "version": 0, + "url": "https://ge.xhamster.com/videos/nudist-driver-takes-me-when-i-was-hitchhiking-outdoor-sex-xh3ucuQ", + "review": 0, + "should_download": 0, + "title": "FKK-Fahrer nimmt mich mit, als ich per Anhalter fuhr. Outdoor-Sex | xHamster", + "file_name": "FKK-Fahrer nimmt mich mit, als ich per Anhalter fuhr. Outdoor-Sex [xh3ucuQ].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/20b95aa4-404b-40f6-bbd2-810d487e88c6.mp4" + }, + { + "id": "20dced78-8d76-4e6b-9139-b65fb362f9d6", + "created_date": "2024-07-25 07:29:46.633229", + "last_modified_date": "2024-07-25 07:29:46.633229", + "version": 0, + "url": "https://ge.xhamster.com/videos/ive-seen-your-dick-twice-maybe-you-should-fuck-me-already-xhpJTq9", + "review": 0, + "should_download": 0, + "title": "Ich habe deinen Schwanz zweimal gesehen, vielleicht solltest du mich schon ficken? | xHamster", + "file_name": "Ich habe deinen Schwanz zweimal gesehen, vielleicht solltest du mich schon ficken\uff1f [xhpJTq9].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/20dced78-8d76-4e6b-9139-b65fb362f9d6.mp4" + }, + { + "id": "20e23a19-e71a-4100-8845-00534b825b5d", + "created_date": "2024-07-25 07:29:45.460706", + "last_modified_date": "2024-07-25 07:29:45.460706", + "version": 0, + "url": "https://ge.xhamster.com/videos/vintage-1979-crazy-orgy-xhPeRmI", + "review": 0, + "should_download": 0, + "title": "Vintage 1979 - verr\u00fcckte Orgie | xHamster", + "file_name": "Vintage 1979 - verr\u00fcckte Orgie [xhPeRmI].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/20e23a19-e71a-4100-8845-00534b825b5d.mp4" + }, + { + "id": "20e9ca38-cf93-4808-8267-9549754961f5", + "created_date": "2024-07-25 07:29:45.793252", + "last_modified_date": "2024-07-25 07:29:45.793252", + "version": 0, + "url": "https://ge.xhamster.com/videos/weltklasse-arsche-5-full-movie-xh0DpwR", + "review": 0, + "should_download": 0, + "title": "Weltklasse Arsche 5 Full Movie, Free Big Cock HD Porn 6e | xHamster", + "file_name": "Weltklasse Arsche 5 (Full Movie) [xh0DpwR].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/20e9ca38-cf93-4808-8267-9549754961f5.mp4" + }, + { + "id": "20f74382-b1c5-4562-b14c-b47f0accfab3", + "created_date": "2024-07-25 07:29:47.816828", + "last_modified_date": "2024-07-25 07:29:47.816828", + "version": 0, + "url": "https://ge.xhamster.com/videos/satisfaction-guaranteed-1976-10204054", + "review": 0, + "should_download": 0, + "title": "Zufriedenheit garantiert (1976) | xHamster", + "file_name": "Zufriedenheit garantiert (1976) [10204054].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/20f74382-b1c5-4562-b14c-b47f0accfab3.mp4" + }, + { + "id": "21e5842b-6ce2-41e5-a6cd-34b7354f2c07", + "created_date": "2025-01-17 16:34:31.698174", + "last_modified_date": "2025-01-17 16:34:31.698181", + "version": 0, + "url": "https://ge.xhamster.com/videos/a-slim-and-beautiful-german-chick-gets-gangbnaged-at-the-bar-xhnwUIG", + "review": 0, + "should_download": 0, + "title": "Ein schlankes und sch\u00f6nes deutsches k\u00fcken wird an der bar gangbang | xHamster", + "file_name": "Ein schlankes und sch\u00f6nes deutsches k\u00fcken wird an der bar gangbang [xhnwUIG].mp4", + "path": null, + "cloud_link": "/data/media/21e5842b-6ce2-41e5-a6cd-34b7354f2c07.mp4" + }, + { + "id": "220842be-5e43-4c3e-8ba3-8b204949188d", + "created_date": "2024-12-29 23:53:27.915735", + "last_modified_date": "2024-12-29 23:53:27.915735", + "version": 0, + "url": "https://ge.xhamster.com/videos/hottest-classics-hd-7-11427446", + "review": 0, + "should_download": 0, + "title": "Hei\u00dfeste Klassiker hd 7 | xHamster", + "file_name": "Hei\u00dfeste Klassiker hd 7 [11427446].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/220842be-5e43-4c3e-8ba3-8b204949188d.mp4" + }, + { + "id": "2210755d-a5a9-4533-946c-6682edafbcf6", + "created_date": "2024-07-25 07:29:44.367215", + "last_modified_date": "2024-07-25 07:29:44.367215", + "version": 0, + "url": "https://ge.xhamster.com/videos/fun-on-comstation-5932436", + "review": 0, + "should_download": 0, + "title": "Spa\u00df auf der Comstation | xHamster", + "file_name": "Spa\u00df auf der Comstation [5932436].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2210755d-a5a9-4533-946c-6682edafbcf6.mp4" + }, + { + "id": "221acfd1-4b7f-4c4a-aeb9-1f4492f6abc5", + "created_date": "2024-07-25 07:29:46.192543", + "last_modified_date": "2024-07-25 07:29:46.192543", + "version": 0, + "url": "https://ge.xhamster.com/videos/mommys-boy-naughty-milf-siri-dahls-caught-naked-in-the-kitchen-pervert-stepson-banged-her-hard-xhIoP39", + "review": 0, + "should_download": 0, + "title": "MAMAS JUNGE - die freche MILF Siri Dahl wird nackt in der K\u00fcche erwischt! Perverser stiefsohn hat sie hart geknallt! | xHamster", + "file_name": "MAMAS JUNGE - die freche MILF Siri Dahl wird nackt in der K\u00fcche erwischt! Perverser stiefsohn hat sie hart geknallt! [xhIoP39].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/221acfd1-4b7f-4c4a-aeb9-1f4492f6abc5.mp4" + }, + { + "id": "224a5257-194e-49ec-9f52-6c27487de93e", + "created_date": "2024-07-25 07:29:47.540061", + "last_modified_date": "2024-07-25 07:29:47.540061", + "version": 0, + "url": "https://ge.xhamster.com/videos/vixen-she-couldn-t-resist-a-naughty-vacation-with-stranger-xhMFSdx", + "review": 0, + "should_download": 0, + "title": "Vixen, sie konnte einem frechen Urlaub mit Fremdem nicht widerstehen | xHamster", + "file_name": "Vixen, sie konnte einem frechen Urlaub mit Fremdem nicht widerstehen [xhMFSdx].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/224a5257-194e-49ec-9f52-6c27487de93e.mp4" + }, + { + "id": "225d6949-f033-4a57-a11d-7b6fe31dc8a6", + "created_date": "2024-07-25 07:29:47.314648", + "last_modified_date": "2024-07-25 07:29:47.314648", + "version": 0, + "url": "https://ge.xhamster.com/videos/fickektesse-11855624", + "review": 0, + "should_download": 0, + "title": "Fickektesse: Free Big Tits Porn Video 2f | xHamster", + "file_name": "fickektesse [11855624].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/225d6949-f033-4a57-a11d-7b6fe31dc8a6.mp4" + }, + { + "id": "226d2e5e-8748-4447-97eb-0ddb2065e4cb", + "created_date": "2024-07-25 07:29:44.277816", + "last_modified_date": "2024-07-25 07:29:44.277816", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-group-sex-1995-11042955", + "review": 0, + "should_download": 0, + "title": "Familien-Gruppensex (1995) | xHamster", + "file_name": "Familien-Gruppensex (1995) [11042955].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/226d2e5e-8748-4447-97eb-0ddb2065e4cb.mp4" + }, + { + "id": "2276ddac-1c0c-4b14-8620-d9ed11ecd00c", + "created_date": "2024-07-25 07:29:47.096888", + "last_modified_date": "2024-07-25 07:29:47.096888", + "version": 0, + "url": "https://ge.xhamster.com/videos/do-you-like-anal-stepsis-analy-fucked-by-hot-stepbro-xh1iSth", + "review": 0, + "should_download": 0, + "title": "\u201eMagst du Analsex?\u201c Stiefschwester wird von hei\u00dfem Stiefbruder anal gefickt | xHamster", + "file_name": "\u201eMagst du Analsex\uff1f\u201c Stiefschwester wird von hei\u00dfem Stiefbruder anal gefickt [xh1iSth].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2276ddac-1c0c-4b14-8620-d9ed11ecd00c.mp4" + }, + { + "id": "2294ddfc-d2e3-48e3-b582-b33977c6a69d", + "created_date": "2024-07-25 07:29:45.543764", + "last_modified_date": "2024-07-25 07:29:45.543764", + "version": 0, + "url": "https://ge.xhamster.com/videos/schluckspechte-full-german-movie-xhfAgzQ", + "review": 0, + "should_download": 0, + "title": "Schluckspechte- Full German Movie, Free Porn b2 | xHamster", + "file_name": "Schluckspechte- full german movie [xhfAgzQ].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/2294ddfc-d2e3-48e3-b582-b33977c6a69d.mp4" + }, + { + "id": "22f4cd27-ddb5-4ef2-865b-68815f92fed2", + "created_date": "2024-07-25 07:29:47.903315", + "last_modified_date": "2024-07-25 07:29:47.903315", + "version": 0, + "url": "https://ge.xhamster.com/videos/pure-taboo-2-step-brothers-dp-their-step-mom-xhCUWQx", + "review": 0, + "should_download": 0, + "title": "Pures Tabu, 2 Stiefbr\u00fcder doppelpenetrieren ihre Stiefmutter | xHamster", + "file_name": "Pures Tabu, 2 Stiefbr\u00fcder doppelpenetrieren ihre Stiefmutter [xhCUWQx].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/22f4cd27-ddb5-4ef2-865b-68815f92fed2.mp4" + }, + { + "id": "232bc320-aab1-4fa4-8688-53ab5c3f6da7", + "created_date": "2024-07-25 07:29:46.896280", + "last_modified_date": "2024-07-25 07:29:46.896280", + "version": 0, + "url": "https://ge.xhamster.com/videos/boat-group-sex-games-xhX5aeg", + "review": 0, + "should_download": 0, + "title": "Boot Gruppensex-Spiele | xHamster", + "file_name": "Boot Gruppensex-Spiele [xhX5aeg].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/232bc320-aab1-4fa4-8688-53ab5c3f6da7.mp4" + }, + { + "id": "2388019e-a4ce-48fd-8e8a-0abb21d38f1c", + "created_date": "2024-07-25 07:29:46.799056", + "last_modified_date": "2024-07-25 07:29:46.799056", + "version": 0, + "url": "https://ge.xhamster.com/videos/new-stepmom-caught-stepson-masturbating-on-cam-xhhcYiq", + "review": 0, + "should_download": 0, + "title": "Neue stiefmutter erwischt stiefsohn beim masturbieren vor der kamera | xHamster", + "file_name": "Neue stiefmutter erwischt stiefsohn beim masturbieren vor der kamera [xhhcYiq].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2388019e-a4ce-48fd-8e8a-0abb21d38f1c.mp4" + }, + { + "id": "23912fb7-f0d4-4a63-b378-fcfb23dee2e2", + "created_date": "2024-09-05 20:03:45.646354", + "last_modified_date": "2024-10-21 16:26:24.771000", + "version": 1, + "url": "https://ge.xhamster.com/videos/chubby-dutch-fucked-on-the-beach-6393203", + "review": 0, + "should_download": 0, + "title": "Mollige Holl\u00e4nderin am Strand gefickt | xHamster", + "file_name": "Mollige Holl\u00e4nderin am Strand gefickt [6393203].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/23912fb7-f0d4-4a63-b378-fcfb23dee2e2.mp4" + }, + { + "id": "2393558b-9373-4fdf-97bf-62e82d7d7b43", + "created_date": "2024-07-25 07:29:46.358094", + "last_modified_date": "2024-07-25 07:29:46.358094", + "version": 0, + "url": "https://ge.xhamster.com/videos/german-brother-and-sister-in-bathroom-10805663", + "review": 0, + "should_download": 0, + "title": "Deutscher Bruder und Schwester im Badezimmer | xHamster", + "file_name": "Deutscher Bruder und Schwester im Badezimmer [10805663].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2393558b-9373-4fdf-97bf-62e82d7d7b43.mp4" + }, + { + "id": "23ca75c8-44f3-456e-abc4-79c0df5d1e3a", + "created_date": "2024-07-25 07:29:47.149097", + "last_modified_date": "2024-07-25 07:29:47.149097", + "version": 0, + "url": "https://ge.xhamster.com/videos/sex-ausflug-der-13-a-2002-13901379", + "review": 0, + "should_download": 0, + "title": "Sex-ausflug Der 13-a 2002, Free European Porn 79 | xHamster", + "file_name": "Sex-Ausflug der 13-A (2002) [13901379].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/23ca75c8-44f3-456e-abc4-79c0df5d1e3a.mp4" + }, + { + "id": "23f2a86e-702f-4b84-a42e-ca0435118b95", + "created_date": "2024-07-25 07:29:44.234351", + "last_modified_date": "2024-07-25 07:29:44.234351", + "version": 0, + "url": "https://ge.xhamster.com/videos/kinky-family-lacy-lennon-my-stepsis-took-my-virginity-13129437", + "review": 0, + "should_download": 0, + "title": "Versaute Familie - Lacy Lennon - meine Stiefschwester hat meine Jungfr\u00e4ulichkeit genommen | xHamster", + "file_name": "Versaute Familie - Lacy Lennon - meine Stiefschwester hat meine Jungfr\u00e4ulichkeit genommen [13129437].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/23f2a86e-702f-4b84-a42e-ca0435118b95.mp4" + }, + { + "id": "2417aa9c-9374-4d3c-952e-cd923ae546f6", + "created_date": "2024-07-25 07:29:45.502079", + "last_modified_date": "2024-07-25 07:29:45.502079", + "version": 0, + "url": "https://ge.xhamster.com/videos/leonora-st-john-british-retro-anal-from-the-1990s-12741986", + "review": 0, + "should_download": 0, + "title": "Leonora St John - britischer Retro-Anal aus den 1990er Jahren | xHamster", + "file_name": "Leonora St John - britischer Retro-Anal aus den 1990er Jahren [12741986].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2417aa9c-9374-4d3c-952e-cd923ae546f6.mp4" + }, + { + "id": "24384b80-08e2-46d9-8925-1c0ece6544cb", + "created_date": "2024-07-25 07:29:47.574262", + "last_modified_date": "2024-07-25 07:29:47.574262", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepbro-walks-in-on-step-sister-in-the-bathtub-masturbating-with-a-big-dildo-sislovesme-xhWB4Bt", + "review": 0, + "should_download": 0, + "title": "Stiefbruder ertritt stiefschwester in der badewanne und masturbiert mit einem gro\u00dfen dildo - sislovesMe | xHamster", + "file_name": "Stiefbruder ertritt stiefschwester in der badewanne und masturbiert mit einem gro\u00dfen dildo - sislovesMe [xhWB4Bt].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/24384b80-08e2-46d9-8925-1c0ece6544cb.mp4" + }, + { + "id": "243fc016-769f-41fb-b965-134adfe468bf", + "created_date": "2024-07-25 07:29:46.813858", + "last_modified_date": "2024-07-25 07:29:46.813858", + "version": 0, + "url": "https://ge.xhamster.com/videos/v-b-11156751", + "review": 0, + "should_download": 0, + "title": "V B: Free Porn Video 76 | xHamster", + "file_name": "V.B. [11156751].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/243fc016-769f-41fb-b965-134adfe468bf.mp4" + }, + { + "id": "248a35ad-729c-485a-bfa3-22b9467a4b25", + "created_date": "2024-07-25 07:29:45.900375", + "last_modified_date": "2024-07-25 07:29:45.900375", + "version": 0, + "url": "https://ge.xhamster.com/videos/corporate-assets-1985-7355919", + "review": 0, + "should_download": 0, + "title": "Unternehmensverm\u00f6gen - 1985 | xHamster", + "file_name": "Unternehmensverm\u00f6gen - 1985 [7355919].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/248a35ad-729c-485a-bfa3-22b9467a4b25.mp4" + }, + { + "id": "2497f0e7-966c-4a76-9a41-b8bd0878365d", + "created_date": "2024-07-25 07:29:47.012200", + "last_modified_date": "2024-07-25 07:29:47.012200", + "version": 0, + "url": "https://ge.xhamster.com/videos/usa-orgies-corporation-vol-03-xhv1eUS", + "review": 0, + "should_download": 0, + "title": "USA Orgien Corporation !!!! - Vol # 03 | xHamster", + "file_name": "USA Orgien Corporation !!!! - Vol # 03 [xhv1eUS].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2497f0e7-966c-4a76-9a41-b8bd0878365d.mp4" + }, + { + "id": "24f5cb78-5882-48f6-a9dc-789b03ec1e17", + "created_date": "2024-07-25 07:29:44.801593", + "last_modified_date": "2024-07-25 07:29:44.801593", + "version": 0, + "url": "https://ge.xhamster.com/videos/blonde-teen-step-sister-gets-a-public-creampie-on-a-boat-12114195", + "review": 0, + "should_download": 0, + "title": "Blondes Teen Stiefschwester bekommt einen \u00f6ffentlichen Creampie auf einem Boot | xHamster", + "file_name": "Blondes Teen Stiefschwester bekommt einen \u00f6ffentlichen Creampie auf einem Boot [12114195].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/24f5cb78-5882-48f6-a9dc-789b03ec1e17.mp4" + }, + { + "id": "24f6a139-1eb8-456f-8d41-8425b912362e", + "created_date": "2024-07-25 07:29:44.477016", + "last_modified_date": "2024-07-25 07:29:44.477016", + "version": 0, + "url": "https://ge.xhamster.com/videos/carolina-sweets-attracted-to-her-step-dads-old-friend-9209254", + "review": 0, + "should_download": 0, + "title": "Carolina Sweets wurde von der alten Freundin ihres Stiefvaters angezogen | xHamster", + "file_name": "Carolina Sweets wurde von der alten Freundin ihres Stiefvaters angezogen [9209254].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/24f6a139-1eb8-456f-8d41-8425b912362e.mp4" + }, + { + "id": "2623d347-2989-405d-bc73-a44e6f59a4f2", + "created_date": "2024-07-25 07:29:45.944756", + "last_modified_date": "2024-07-25 07:29:45.944756", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-old-man-fucked-a-young-librarian-9650252", + "review": 0, + "should_download": 0, + "title": "Der alte Mann fickte eine junge Bibliothekarin | xHamster", + "file_name": "Der alte Mann fickte eine junge Bibliothekarin [9650252].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2623d347-2989-405d-bc73-a44e6f59a4f2.mp4" + }, + { + "id": "263237bf-ae86-4c8a-98bc-66897382652e", + "created_date": "2024-07-25 07:29:47.228106", + "last_modified_date": "2024-07-25 07:29:47.228106", + "version": 0, + "url": "https://ge.xhamster.com/videos/adult-time-riley-reids-insane-threesome-with-her-stepdad-teen-bff-janice-griffith-part-1-and-2-xhzJOOG", + "review": 0, + "should_download": 0, + "title": "ERWACHSENENZEIT - Riley Reids WAHNSINNIGER DREIER mit ihrem stiefvater & teen BFF Janice Griffith! TEIL 1 und 2 | xHamster", + "file_name": "ERWACHSENENZEIT - Riley Reids WAHNSINNIGER DREIER mit ihrem stiefvater & teen BFF Janice Griffith! TEIL 1 und 2 [xhzJOOG].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/263237bf-ae86-4c8a-98bc-66897382652e.mp4" + }, + { + "id": "266853c1-caed-42d7-853e-183fb23c6014", + "created_date": "2024-07-25 07:29:46.580498", + "last_modified_date": "2024-07-25 07:29:46.580498", + "version": 0, + "url": "https://ge.xhamster.com/videos/you-were-gonna-fuck-my-friend-s15-e6-xhaitwW", + "review": 0, + "should_download": 0, + "title": "Du wirst meinen freund ficken - S15: e6 | xHamster", + "file_name": "Du wirst meinen freund ficken - S15\uff1a e6 [xhaitwW].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/266853c1-caed-42d7-853e-183fb23c6014.mp4" + }, + { + "id": "26b04ee7-3fcb-4be6-8c1f-036605236bcc", + "created_date": "2024-07-25 07:29:44.377828", + "last_modified_date": "2024-07-25 07:29:44.377828", + "version": 0, + "url": "https://ge.xhamster.com/videos/geschichte-der-o-episode-1-6273303", + "review": 0, + "should_download": 0, + "title": "Geschichte Der O - Episode 1, Free German Porn aa | xHamster", + "file_name": "Geschichte der O - Episode 1 [6273303].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/26b04ee7-3fcb-4be6-8c1f-036605236bcc.mp4" + }, + { + "id": "26c47473-2953-4315-a180-fff574db53ce", + "created_date": "2024-12-29 23:53:27.918420", + "last_modified_date": "2024-12-29 23:53:27.918420", + "version": 0, + "url": "https://ge.xhamster.com/videos/they-love-kissing-each-other-with-my-cum-on-their-lips-xhTGRvJb", + "review": 0, + "should_download": 0, + "title": "Sie lieben es, sich gegenseitig mit meinem sperma auf ihren lippen zu k\u00fcssen | xHamster", + "file_name": "Sie lieben es, sich gegenseitig mit meinem sperma auf ihren lippen zu k\u00fcssen [xhTGRvJb].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/26c47473-2953-4315-a180-fff574db53ce.mp4" + }, + { + "id": "271219bf-b1d4-4828-bea8-cf1a9db3e81b", + "created_date": "2024-07-25 07:29:47.450783", + "last_modified_date": "2024-07-25 07:29:47.450783", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-swinger-neighbors-make-noises-comp-xh7Tkrr", + "review": 0, + "should_download": 0, + "title": "Meine Swinger-Nachbarn machen Ger\u00e4usche | xHamster", + "file_name": "Meine Swinger-Nachbarn machen Ger\u00e4usche [xh7Tkrr].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/271219bf-b1d4-4828-bea8-cf1a9db3e81b.mp4" + }, + { + "id": "274bbe8a-819a-412b-89ef-cac2c692b18e", + "created_date": "2024-07-25 07:29:45.778732", + "last_modified_date": "2024-07-25 07:29:45.778732", + "version": 0, + "url": "https://ge.xhamster.com/videos/like-mother-like-daughter-1973-restored-7766219", + "review": 0, + "should_download": 0, + "title": "Wie Mutter, wie Tochter - 1973 (restauriert) | xHamster", + "file_name": "Wie Mutter, wie Tochter - 1973 (restauriert) [7766219].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/274bbe8a-819a-412b-89ef-cac2c692b18e.mp4" + }, + { + "id": "2765fb44-c458-461a-959b-80b2531561b8", + "created_date": "2024-07-25 07:29:47.567314", + "last_modified_date": "2024-07-25 07:29:47.567314", + "version": 0, + "url": "https://ge.xhamster.com/videos/foursome-with-dp-3882121", + "review": 0, + "should_download": 0, + "title": "Vierer mit Doppelpenetration | xHamster", + "file_name": "Vierer mit Doppelpenetration [3882121].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2765fb44-c458-461a-959b-80b2531561b8.mp4" + }, + { + "id": "28319815-217e-449c-9d98-82574e534e9d", + "created_date": "2024-07-25 07:29:45.290059", + "last_modified_date": "2024-09-06 09:41:02.745000", + "version": 1, + "url": "https://ge.xhamster.com/videos/lusty-boarding-school-xhGBNwZ", + "review": 0, + "should_download": 0, + "title": "Lustvolles Internat", + "file_name": "Lustvolles Internat [xhGBNwZ].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/28319815-217e-449c-9d98-82574e534e9d.mp4" + }, + { + "id": "287256a2-47c2-4170-90eb-c9a6bfc4973f", + "created_date": "2024-07-25 07:29:44.824928", + "last_modified_date": "2024-07-25 07:29:44.824928", + "version": 0, + "url": "https://ge.xhamster.com/videos/memoiren-der-lust-1979-6285457", + "review": 0, + "should_download": 0, + "title": "Memoiren Der Lust 1979, Free Orgy Porn Video f3 | xHamster", + "file_name": "Memoiren der Lust (1979) [6285457].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/287256a2-47c2-4170-90eb-c9a6bfc4973f.mp4" + }, + { + "id": "28b9c9a9-45bf-4128-8533-7f2c50026fcd", + "created_date": "2024-07-25 07:29:47.794082", + "last_modified_date": "2024-07-25 07:29:47.794082", + "version": 0, + "url": "https://ge.xhamster.com/videos/young-sluts-in-search-of-love-full-movie-xhtYq8y", + "review": 0, + "should_download": 0, + "title": "Junge Schlampen auf der Suche nach Liebe (kompletter Film) | xHamster", + "file_name": "Junge Schlampen auf der Suche nach Liebe (kompletter Film) [xhtYq8y].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/28b9c9a9-45bf-4128-8533-7f2c50026fcd.mp4" + }, + { + "id": "28f45aa0-5177-4419-98d1-bfd2617201da", + "created_date": "2024-07-25 07:29:47.620909", + "last_modified_date": "2024-07-25 07:29:47.620909", + "version": 0, + "url": "https://ge.xhamster.com/videos/busty-redhead-taxi-cutie-assfucked-by-driver-5448301", + "review": 0, + "should_download": 0, + "title": "Vollbusige rothaarige Taxi-S\u00fc\u00dfe vom Fahrer arschgefickt | xHamster", + "file_name": "Vollbusige rothaarige Taxi-S\u00fc\u00dfe vom Fahrer arschgefickt [5448301].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/28f45aa0-5177-4419-98d1-bfd2617201da.mp4" + }, + { + "id": "293e5d1a-260d-42fa-ae02-54e20f6d866f", + "created_date": "2024-07-25 07:29:47.105424", + "last_modified_date": "2024-07-25 07:29:47.105424", + "version": 0, + "url": "https://ge.xhamster.com/videos/cant-you-see-im-busy-wait-do-you-have-a-boner-britt-blair-asks-stepbro-s28-e3-xhWtQpv", + "review": 0, + "should_download": 0, + "title": "\"Kannst du nicht sehen, dass ich besch\u00e4ftigt bin? Warte, hast du eine Latte ?!\" Britt Blair fragt Stiefbruder -s28: e3 | xHamster", + "file_name": "\uff02Kannst du nicht sehen, dass ich besch\u00e4ftigt bin\uff1f Warte, hast du eine Latte \uff1f!\uff02 Britt Blair fragt Stiefbruder -s28\uff1a e3 [xhWtQpv].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/293e5d1a-260d-42fa-ae02-54e20f6d866f.mp4" + }, + { + "id": "29900405-ec86-4675-9524-fffb200940cb", + "created_date": "2024-07-25 07:29:44.808581", + "last_modified_date": "2024-07-25 07:29:44.808581", + "version": 0, + "url": "https://ge.xhamster.com/videos/gangbanged-wife-xhKL12h", + "review": 0, + "should_download": 0, + "title": "GANGBANGED WIFE | xHamster", + "file_name": "GANGBANGED WIFE [xhKL12h].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/29900405-ec86-4675-9524-fffb200940cb.mp4" + }, + { + "id": "29d471ea-4fbf-4860-adc1-47291f74e028", + "created_date": "2024-10-07 20:47:56.428026", + "last_modified_date": "2024-10-21 16:26:31.598000", + "version": 1, + "url": "https://ge.xhamster.com/videos/18-videoz-partying-drinking-and-fucking-6951146", + "review": 0, + "should_download": 0, + "title": "18 Videoz - feiern, trinken und ficken | xHamster", + "file_name": "18 Videoz - feiern, trinken und ficken [6951146].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/29d471ea-4fbf-4860-adc1-47291f74e028.mp4" + }, + { + "id": "2a7247ab-27b8-46e4-b951-29ed896e2342", + "created_date": "2024-07-25 07:29:46.269438", + "last_modified_date": "2024-07-25 07:29:46.269438", + "version": 0, + "url": "https://ge.xhamster.com/videos/dienst-imhausechicfick-xh6TBnP", + "review": 0, + "should_download": 0, + "title": "Dienst Imhausechicfick, Free Retro Porn Video ed | xHamster", + "file_name": "Dienst imHauseChicfick [xh6TBnP].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2a7247ab-27b8-46e4-b951-29ed896e2342.mp4" + }, + { + "id": "2ad403b9-c2b9-4f40-8dc3-4eacda37a5bd", + "created_date": "2024-07-25 07:29:44.805011", + "last_modified_date": "2024-07-25 07:29:44.805011", + "version": 0, + "url": "https://ge.xhamster.com/videos/girl-double-penetrated-while-boat-trip-2674786", + "review": 0, + "should_download": 0, + "title": "M\u00e4dchen doppelt penetriert w\u00e4hrend Bootsfahrt | xHamster", + "file_name": "M\u00e4dchen doppelt penetriert w\u00e4hrend Bootsfahrt [2674786].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2ad403b9-c2b9-4f40-8dc3-4eacda37a5bd.mp4" + }, + { + "id": "2b1d148f-1e0f-48d6-aecf-6af31a62ce90", + "created_date": "2024-07-25 07:29:45.161561", + "last_modified_date": "2024-07-25 07:29:45.161561", + "version": 0, + "url": "https://ge.xhamster.com/videos/horny-female-teacher-has-threesome-with-schoolgirl-and-boy-student-in-class-xhyzF3P", + "review": 0, + "should_download": 0, + "title": "Geile lehrerin hat dreier mit schulm\u00e4dchen und sch\u00fcler in der klasse | xHamster", + "file_name": "Geile lehrerin hat dreier mit schulm\u00e4dchen und sch\u00fcler in der klasse [xhyzF3P].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2b1d148f-1e0f-48d6-aecf-6af31a62ce90.mp4" + }, + { + "id": "2b4e6588-c87f-47ad-8e36-ed525054ab92", + "created_date": "2024-07-25 07:29:45.325877", + "last_modified_date": "2024-07-25 07:29:45.325877", + "version": 0, + "url": "https://ge.xhamster.com/videos/mother-step-daughter-learn-to-share-kara-lee-dava-foxx-14933477", + "review": 0, + "should_download": 0, + "title": "Mutter und Stieftochter lernen, zu teilen - Kara Lee & Dava Foxx | xHamster", + "file_name": "Mutter und Stieftochter lernen, zu teilen - Kara Lee & Dava Foxx [14933477].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/2b4e6588-c87f-47ad-8e36-ed525054ab92.mp4" + }, + { + "id": "2b79e015-e533-4fbb-b48d-69d5aedcac97", + "created_date": "2024-07-25 07:29:45.715342", + "last_modified_date": "2024-07-25 07:29:45.715342", + "version": 0, + "url": "https://ge.xhamster.com/videos/boarding-school-1970-10269405", + "review": 0, + "should_download": 0, + "title": "Internat (1970) | xHamster", + "file_name": "Internat (1970) [10269405].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2b79e015-e533-4fbb-b48d-69d5aedcac97.mp4" + }, + { + "id": "2c97b96a-d623-4775-b994-d8f80325123c", + "created_date": "2024-07-25 07:29:46.315242", + "last_modified_date": "2024-07-25 07:29:46.315242", + "version": 0, + "url": "https://ge.xhamster.com/videos/pool-party-orgy-with-a-bunch-of-horny-teens-bitches-11135779", + "review": 0, + "should_download": 0, + "title": "Poolparty-Orgie mit ein paar geilen Teenager-Schlampen | xHamster", + "file_name": "Poolparty-Orgie mit ein paar geilen Teenager-Schlampen [11135779].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2c97b96a-d623-4775-b994-d8f80325123c.mp4" + }, + { + "id": "2c993c58-dcb5-447e-ba38-5e05f5846525", + "created_date": "2024-07-25 07:29:44.594440", + "last_modified_date": "2024-07-25 07:29:44.594440", + "version": 0, + "url": "https://ge.xhamster.com/videos/first-sexual-experience-at-the-lake-2668426", + "review": 0, + "should_download": 0, + "title": "Erste sexuelle Erfahrung am See | xHamster", + "file_name": "Erste sexuelle Erfahrung am See [2668426].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2c993c58-dcb5-447e-ba38-5e05f5846525.mp4" + }, + { + "id": "2ca47897-9822-416f-a803-ef1c6bcefec2", + "created_date": "2024-07-25 07:29:44.946276", + "last_modified_date": "2025-01-03 11:56:22.273000", + "version": 1, + "url": "https://ge.xhamster.com/videos/ill-show-you-my-pussy-if-you-show-me-your-dick-lila-love-dares-stepbro-s26-e12-xhWyjzC", + "review": 0, + "should_download": 0, + "title": "\"Ich zeige dir meine Muschi, wenn .. du mir deinen Schwanz zeigst\" lila Liebe wagt Stiefbruder - s26: e12 | xHamster", + "file_name": "\uff02Ich zeige dir meine Muschi, wenn .. du mir deinen Schwanz zeigst\uff02 lila Liebe wagt Stiefbruder - s26\uff1a e12 [xhWyjzC].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2ca47897-9822-416f-a803-ef1c6bcefec2.mp4" + }, + { + "id": "2cc7c79a-4e48-4452-b2c6-03ffd9a64b3a", + "created_date": "2024-07-25 07:29:45.685134", + "last_modified_date": "2024-07-25 07:29:45.685134", + "version": 0, + "url": "https://ge.xhamster.com/videos/couples-play-spin-the-bottle-but-it-turns-into-fuck-the-neighbors-xh1CNyO", + "review": 0, + "should_download": 0, + "title": "Paare spielen, drehen die Flasche, aber es wird zu einem Fick der Nachbarn | xHamster", + "file_name": "Paare spielen, drehen die Flasche, aber es wird zu einem Fick der Nachbarn [xh1CNyO].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/2cc7c79a-4e48-4452-b2c6-03ffd9a64b3a.mp4" + }, + { + "id": "2ccdaf98-c4ed-44ad-bd49-28c8c89f309d", + "created_date": "2024-07-25 07:29:45.766806", + "last_modified_date": "2024-07-25 07:29:45.766806", + "version": 0, + "url": "https://ge.xhamster.com/videos/sniffing-the-babysitters-dirty-panties-leads-to-cheating-11394407", + "review": 0, + "should_download": 0, + "title": "Das schmutzige H\u00f6schen des Babysitters zu schn\u00fcffeln, f\u00fchrt zum Betr\u00fcgen! | xHamster", + "file_name": "Das schmutzige H\u00f6schen des Babysitters zu schn\u00fcffeln, f\u00fchrt zum Betr\u00fcgen! [11394407].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2ccdaf98-c4ed-44ad-bd49-28c8c89f309d.mp4" + }, + { + "id": "2cf47c13-3808-4520-a07a-ddad147b1afe", + "created_date": "2024-07-25 07:29:46.133668", + "last_modified_date": "2024-07-25 07:29:46.133668", + "version": 0, + "url": "https://ge.xhamster.com/videos/aerobic-sex-im-fickness-center-1988-german-dvd-rip-full-xhWRXlQ", + "review": 0, + "should_download": 0, + "title": "Aerobes Sex im Fickness Center (1988, deutsch, DVD Rip, voll) | xHamster", + "file_name": "Aerobes Sex im Fickness Center (1988, deutsch, DVD Rip, voll) [xhWRXlQ].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2cf47c13-3808-4520-a07a-ddad147b1afe.mp4" + }, + { + "id": "2d185929-bb75-40ae-8a14-cc8e9c76b0a8", + "created_date": "2024-07-25 07:29:44.522554", + "last_modified_date": "2024-07-25 07:29:44.522554", + "version": 0, + "url": "https://ge.xhamster.com/videos/american-college-xxx-the-originalin-hd-story-n-22-xhZhoI7", + "review": 0, + "should_download": 0, + "title": "Amerikanisches College xxx !!! - (das Original in HD) - Geschichte n. # 22 | xHamster", + "file_name": "Amerikanisches College xxx !!! - (das Original in HD) - Geschichte n. # 22 [xhZhoI7].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2d185929-bb75-40ae-8a14-cc8e9c76b0a8.mp4" + }, + { + "id": "2d1c3649-38a5-4320-8898-6c14cb0b67de", + "created_date": "2024-07-25 07:29:46.158584", + "last_modified_date": "2024-07-25 07:29:46.158584", + "version": 0, + "url": "https://ge.xhamster.com/videos/two-cocks-for-a-french-milf-in-heat-shes-so-horny-2317966", + "review": 0, + "should_download": 0, + "title": "Zwei Schw\u00e4nze f\u00fcr eine franz\u00f6sische MILF, die hei\u00df ist, sie ist so geil! | xHamster", + "file_name": "Zwei Schw\u00e4nze f\u00fcr eine franz\u00f6sische MILF, die hei\u00df ist, sie ist so geil! [2317966].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2d1c3649-38a5-4320-8898-6c14cb0b67de.mp4" + }, + { + "id": "2d2a8e85-0567-4798-b3a1-664ef7e96ec5", + "created_date": "2024-11-10 16:53:33.494244", + "last_modified_date": "2024-11-10 16:53:33.494244", + "version": 0, + "url": "https://ge.xhamster.com/videos/summer-vacation-family-hardcore-fucking-in-a-nice-villa-film-13823722", + "review": 0, + "should_download": 0, + "title": "Sommerferien Familien-Hardcore-Ficken in einem sch\u00f6nen Villenfilm | xHamster", + "file_name": "Sommerferien Familien-Hardcore-Ficken in einem sch\u00f6nen Villenfilm [13823722].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/2d2a8e85-0567-4798-b3a1-664ef7e96ec5.mp4" + }, + { + "id": "2d41e9bd-e5e0-4d31-9240-29d7c751e230", + "created_date": "2024-07-25 07:29:46.603654", + "last_modified_date": "2024-07-25 07:29:46.603654", + "version": 0, + "url": "https://ge.xhamster.com/videos/18-and-confused-1-xhkzIdN", + "review": 0, + "should_download": 0, + "title": "18 und verwirrt # 1 | xHamster", + "file_name": "18 und verwirrt # 1 [xhkzIdN].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2d41e9bd-e5e0-4d31-9240-29d7c751e230.mp4" + }, + { + "id": "2d66b335-4284-4ac9-95c3-1857393f476e", + "created_date": "2024-07-25 07:29:45.301022", + "last_modified_date": "2024-07-25 07:29:45.301022", + "version": 0, + "url": "https://ge.xhamster.com/videos/sweetsinner-step-siblings-unleash-pent-up-sexual-frustration-11856436", + "review": 0, + "should_download": 0, + "title": "Sweetsinner, Stiefgeschwister, entfesselt sexuelle Frustration | xHamster", + "file_name": "Sweetsinner, Stiefgeschwister, entfesselt sexuelle Frustration [11856436].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2d66b335-4284-4ac9-95c3-1857393f476e.mp4" + }, + { + "id": "2d7bad5b-1dcb-46ff-9e30-fa418b45d55a", + "created_date": "2024-07-25 07:29:44.510746", + "last_modified_date": "2024-07-25 07:29:44.510746", + "version": 0, + "url": "https://ge.xhamster.com/videos/die-geile-professorin-1976-13453153", + "review": 0, + "should_download": 0, + "title": "Die Geile Professorin 1976, Free European Porn b8 | xHamster", + "file_name": "Die Geile Professorin (1976) [13453153].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2d7bad5b-1dcb-46ff-9e30-fa418b45d55a.mp4" + }, + { + "id": "2dcd7d04-7861-4586-8d06-68c2396378ae", + "created_date": "2024-07-25 07:29:45.429862", + "last_modified_date": "2024-07-25 07:29:45.429862", + "version": 0, + "url": "https://ge.xhamster.com/videos/cartoon-porn-from-cartoonvalley-part-1-903113", + "review": 0, + "should_download": 0, + "title": "Cartoon-Porno aus Cartoonvalley Teil 1 | xHamster", + "file_name": "Cartoon-Porno aus Cartoonvalley Teil 1 [903113].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2dcd7d04-7861-4586-8d06-68c2396378ae.mp4" + }, + { + "id": "2e6db25c-2fdd-420c-8d63-6a101ef70bae", + "created_date": "2024-07-25 07:29:47.990030", + "last_modified_date": "2024-07-25 07:29:47.990030", + "version": 0, + "url": "https://ge.xhamster.com/videos/schone-bescherung-xhVNYIz", + "review": 0, + "should_download": 0, + "title": "Sch\u00f6ne \u00dcberraschung | xHamster", + "file_name": "Sch\u00f6ne \u00dcberraschung [xhVNYIz].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2e6db25c-2fdd-420c-8d63-6a101ef70bae.mp4" + }, + { + "id": "2e980db1-a41f-45d9-8eec-fa4e9394d48f", + "created_date": "2024-07-25 07:29:44.918047", + "last_modified_date": "2024-07-25 07:29:44.918047", + "version": 0, + "url": "https://ge.xhamster.com/videos/naughty-girls-5520628", + "review": 0, + "should_download": 0, + "title": "Freches M\u00e4dchen | xHamster", + "file_name": "Freches M\u00e4dchen [5520628].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/2e980db1-a41f-45d9-8eec-fa4e9394d48f.mp4" + }, + { + "id": "2f1dfa43-d8c8-4955-a683-26221a91bb05", + "created_date": "2024-07-25 07:29:47.859123", + "last_modified_date": "2024-07-25 07:29:47.859123", + "version": 0, + "url": "https://ge.xhamster.com/videos/when-your-boss-invites-you-to-a-gangbang-and-the-husband-doesnt-know-about-it-xhcy4QQ", + "review": 0, + "should_download": 0, + "title": "Wenn dein Chef zum Gangbang einl\u00e4dt und der Ehemann wei\u00df davon nichts | xHamster", + "file_name": "Wenn dein Chef zum Gangbang einl\u00e4dt und der Ehemann wei\u00df davon nichts [xhcy4QQ].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2f1dfa43-d8c8-4955-a683-26221a91bb05.mp4" + }, + { + "id": "2f4914b5-7511-4570-bd5a-4bc91a585655", + "created_date": "2024-07-25 07:29:44.898414", + "last_modified_date": "2024-07-25 07:29:44.898414", + "version": 0, + "url": "https://ge.xhamster.com/videos/college-fun-xh1zEsF", + "review": 0, + "should_download": 0, + "title": "College-Spa\u00df | xHamster", + "file_name": "College-Spa\u00df [xh1zEsF].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2f4914b5-7511-4570-bd5a-4bc91a585655.mp4" + }, + { + "id": "2fd0569d-a3ab-44cf-9423-0b8096c6e65e", + "created_date": "2024-07-25 07:29:47.870576", + "last_modified_date": "2024-07-25 07:29:47.870576", + "version": 0, + "url": "https://ge.xhamster.com/videos/sunde-sex-und-scharfe-katzen-1980-german-full-movie-dvd-xhHPFex", + "review": 0, + "should_download": 0, + "title": "Sunde Sex Und Scharfe Katzen 1980 German Full Movie Dvd | xHamster", + "file_name": "Sunde, Sex und scharfe Katzen (1980, German full movie, DVD) [xhHPFex].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2fd0569d-a3ab-44cf-9423-0b8096c6e65e.mp4" + }, + { + "id": "2fecc4bf-4cfd-4211-a0da-74b05427d744", + "created_date": "2024-07-25 07:29:47.700456", + "last_modified_date": "2024-07-25 07:29:47.700456", + "version": 0, + "url": "https://ge.xhamster.com/videos/super-attractive-german-chicks-getting-fucked-in-the-hot-tub-xhZUpFG", + "review": 0, + "should_download": 0, + "title": "Super attraktive deutsche K\u00fcken werden im Whirlpool gefickt | xHamster", + "file_name": "Super attraktive deutsche K\u00fcken werden im Whirlpool gefickt [xhZUpFG].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/2fecc4bf-4cfd-4211-a0da-74b05427d744.mp4" + }, + { + "id": "3052cbc6-7f0b-4b93-bd9e-188bf7e006cd", + "created_date": "2024-08-09 21:38:48.456721", + "last_modified_date": "2024-08-16 10:27:47.462000", + "version": 1, + "url": "https://ge.xhamster.com/videos/old-childhood-love-is-suddenly-at-the-door-xhBrKaD", + "review": 0, + "should_download": 0, + "title": "Alte Jugendliebe steht pl\u00f6tzlich vor der T\u00fcr! | xHamster", + "file_name": "Alte Jugendliebe steht pl\u00f6tzlich vor der T\u00fcr! [xhBrKaD].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/3052cbc6-7f0b-4b93-bd9e-188bf7e006cd.mp4" + }, + { + "id": "30c74f01-e37a-43c8-b247-171a1c167753", + "created_date": "2024-07-25 07:29:45.560139", + "last_modified_date": "2024-07-25 07:29:45.560139", + "version": 0, + "url": "https://ge.xhamster.com/videos/paolas-nude-blind-date-she-loves-being-fucked-outdoors-xh8T7ab", + "review": 0, + "should_download": 0, + "title": "Paolas nacktes Blind Date. Sie liebt es, im Freien gefickt zu werden! | xHamster", + "file_name": "Paolas nacktes Blind Date. Sie liebt es, im Freien gefickt zu werden! [xh8T7ab].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/30c74f01-e37a-43c8-b247-171a1c167753.mp4" + }, + { + "id": "30f949ed-5520-4e7d-8b05-d0981bdbf591", + "created_date": "2024-07-25 07:29:47.454322", + "last_modified_date": "2024-07-25 07:29:47.454322", + "version": 0, + "url": "https://ge.xhamster.com/videos/fick-personal-1993-xhzEPZk", + "review": 0, + "should_download": 0, + "title": "Fuck Staff (1993) | xHamster", + "file_name": "Fuck Staff (1993) [xhzEPZk].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/30f949ed-5520-4e7d-8b05-d0981bdbf591.mp4" + }, + { + "id": "3140e9e2-685b-4cc3-95f7-8605d7c90cc8", + "created_date": "2024-07-25 07:29:46.618520", + "last_modified_date": "2024-07-25 07:29:46.618520", + "version": 0, + "url": "https://ge.xhamster.com/videos/sophomore-babes-outdoors-beach-sex-orgy-6253790", + "review": 0, + "should_download": 0, + "title": "Sophomore Sch\u00e4tzchen im Freien Strand-Sex-Orgie | xHamster", + "file_name": "Sophomore Sch\u00e4tzchen im Freien Strand-Sex-Orgie [6253790].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3140e9e2-685b-4cc3-95f7-8605d7c90cc8.mp4" + }, + { + "id": "316910a1-5895-42b5-85f3-affe8cb1d7fc", + "created_date": "2024-10-21 15:08:43.557416", + "last_modified_date": "2024-10-21 16:26:43.304000", + "version": 1, + "url": "https://ge.xhamster.com/videos/outdoor-foursome-with-greta-milos-1862549", + "review": 0, + "should_download": 0, + "title": "Outdoor-Vierer mit Greta Milos | xHamster", + "file_name": "Outdoor-Vierer mit Greta Milos [1862549].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/316910a1-5895-42b5-85f3-affe8cb1d7fc.mp4" + }, + { + "id": "3189b25b-cdaf-46c6-be88-fea733df28f3", + "created_date": "2024-07-25 07:29:44.385025", + "last_modified_date": "2024-07-25 07:29:44.385025", + "version": 0, + "url": "https://ge.xhamster.com/videos/orgy-with-sperm-eating-and-snowballing-milfs-xh3iRBm", + "review": 0, + "should_download": 0, + "title": "Orgy with Sperm Eating and Snowballing MILFs: Free Porn 2c | xHamster", + "file_name": "Orgy with sperm eating and snowballing milfs [xh3iRBm].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/3189b25b-cdaf-46c6-be88-fea733df28f3.mp4" + }, + { + "id": "319852c5-8256-4fc3-829f-13d70e5e8c96", + "created_date": "2024-07-25 07:29:46.026573", + "last_modified_date": "2024-07-25 07:29:46.026573", + "version": 0, + "url": "https://ge.xhamster.com/videos/thick-ass-redhead-with-big-natural-tits-and-braces-gets-fucked-xhj06Sq", + "review": 0, + "should_download": 0, + "title": "Rothaarige mit dickem Arsch mit gro\u00dfen nat\u00fcrlichen Titten und Zahnspange wird gefickt | xHamster", + "file_name": "Rothaarige mit dickem Arsch mit gro\u00dfen nat\u00fcrlichen Titten und Zahnspange wird gefickt [xhj06Sq].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/319852c5-8256-4fc3-829f-13d70e5e8c96.mp4" + }, + { + "id": "31e206f5-9f82-4e42-84db-9958254eed26", + "created_date": "2024-07-25 07:29:45.014255", + "last_modified_date": "2024-07-25 07:29:45.014255", + "version": 0, + "url": "https://ge.xhamster.com/videos/hot-tub-orgy-at-the-neighbors-house-12975432", + "review": 0, + "should_download": 0, + "title": "Whirlpool-Orgie im Haus des Nachbarn | xHamster", + "file_name": "Whirlpool-Orgie im Haus des Nachbarn [12975432].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/31e206f5-9f82-4e42-84db-9958254eed26.mp4" + }, + { + "id": "31ffe280-bb1a-4bdb-8c4d-652ee4701099", + "created_date": "2024-07-25 07:29:47.560258", + "last_modified_date": "2024-07-25 07:29:47.560258", + "version": 0, + "url": "https://ge.xhamster.com/videos/teeny-exzesse-40-sperma-spiele-1996-xh5V9J3", + "review": 0, + "should_download": 0, + "title": "Teeny Exzesse 40 Sperma-spiele 1996, Free Porn 87 | xHamster", + "file_name": "Teeny Exzesse 40\uff1a Sperma-Spiele (1996) [xh5V9J3].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/31ffe280-bb1a-4bdb-8c4d-652ee4701099.mp4" + }, + { + "id": "323e80af-34be-44d7-a43e-9367e32ea33f", + "created_date": "2024-07-25 07:29:45.609439", + "last_modified_date": "2024-07-25 07:29:45.609439", + "version": 0, + "url": "https://ge.xhamster.com/videos/how-i-fucked-your-mother-episode-1-xhqGISD", + "review": 0, + "should_download": 0, + "title": "Wie ich deine Mutter gefickt habe - Episode 1 | xHamster", + "file_name": "Wie ich deine Mutter gefickt habe - Episode 1 [xhqGISD].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/323e80af-34be-44d7-a43e-9367e32ea33f.mp4" + }, + { + "id": "3240bb9b-0afb-4ca1-a581-ca0da71cd32e", + "created_date": "2024-07-25 07:29:44.341865", + "last_modified_date": "2024-07-25 07:29:44.341865", + "version": 0, + "url": "https://ge.xhamster.com/videos/adult-time-milf-masseuse-chanel-preston-introduces-bunny-colby-her-bf-to-3-way-nuru-massage-xhy7CTK", + "review": 0, + "should_download": 0, + "title": "ADULT TIME - MILF Masseuse Chanel Preston stellt Bunny colby und ihren freund zur 3-Wege-Nuru-Massage vor! | xHamster", + "file_name": "ADULT TIME - MILF Masseuse Chanel Preston stellt Bunny colby und ihren freund zur 3-Wege-Nuru-Massage vor! [xhy7CTK].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3240bb9b-0afb-4ca1-a581-ca0da71cd32e.mp4" + }, + { + "id": "327bf07f-8dfa-46d8-978d-5270ff125733", + "created_date": "2024-07-25 07:29:47.239338", + "last_modified_date": "2024-07-25 07:29:47.239338", + "version": 0, + "url": "https://ge.xhamster.com/videos/vanna-bardot-tells-stepbro-im-going-to-fuck-your-brains-out-tonight-s4-e9-xhyqKee", + "review": 0, + "should_download": 0, + "title": "Vanna Bardot sagt Stiefbruder: \"Ich werde heute Abend dein Gehirn ausficken\" - s4: e9 | xHamster", + "file_name": "Vanna Bardot sagt Stiefbruder\uff1a \uff02Ich werde heute Abend dein Gehirn ausficken\uff02 - s4\uff1a e9 [xhyqKee].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/327bf07f-8dfa-46d8-978d-5270ff125733.mp4" + }, + { + "id": "32914e4d-1e00-4542-9004-448cd947076c", + "created_date": "2024-08-09 21:18:37.987070", + "last_modified_date": "2024-08-16 10:29:12.596000", + "version": 1, + "url": "https://ge.xhamster.com/videos/no-shame-at-all-7239456", + "review": 0, + "should_download": 0, + "title": "Keine Schande | xHamster", + "file_name": "Keine Schande [7239456].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/32914e4d-1e00-4542-9004-448cd947076c.mp4" + }, + { + "id": "32ecb94b-8cb5-4055-af2c-5c164e187c37", + "created_date": "2024-07-25 07:29:45.525601", + "last_modified_date": "2024-07-25 07:29:45.525601", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepmom-goes-alpha-s18-e8-xhA7Ryk", + "review": 0, + "should_download": 0, + "title": "Stiefmutter goes alpha - s18: e8 | xHamster", + "file_name": "Stiefmutter goes alpha - s18\uff1a e8 [xhA7Ryk].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/32ecb94b-8cb5-4055-af2c-5c164e187c37.mp4" + }, + { + "id": "3395d2fe-c977-4b7e-8cb7-261c137d3f48", + "created_date": "2024-12-29 23:53:27.931785", + "last_modified_date": "2024-12-29 23:53:27.931785", + "version": 0, + "url": "https://ge.xhamster.com/videos/mommy-his-dick-is-stuck-in-a-vacuum-cleaner-s13-e4-xh3bfBxZ", + "review": 0, + "should_download": 0, + "title": "Mommy His Dick is Stuck in a Vacuum Cleaner - S13 E4 | xHamster", + "file_name": "Mommy His Dick Is Stuck in a Vacuum Cleaner - S13\uff1ae4 [xh3bfBxZ].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/3395d2fe-c977-4b7e-8cb7-261c137d3f48.mp4" + }, + { + "id": "3399a784-80e5-4302-b309-f73eecf9f24d", + "created_date": "2024-07-25 07:29:45.879963", + "last_modified_date": "2024-07-25 07:29:45.879963", + "version": 0, + "url": "https://ge.xhamster.com/videos/amateur-passionate-sex-on-the-boat-like-in-a-dream-xhuIura", + "review": 0, + "should_download": 0, + "title": "Amateur leidenschaftlicher SEX auf dem BOOT wie im Traum | xHamster", + "file_name": "Amateur leidenschaftlicher SEX auf dem BOOT wie im Traum [xhuIura].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3399a784-80e5-4302-b309-f73eecf9f24d.mp4" + }, + { + "id": "3415ace5-9945-41c1-8fea-17bf6c85fe0f", + "created_date": "2024-07-25 07:29:46.238729", + "last_modified_date": "2024-07-25 07:29:46.238729", + "version": 0, + "url": "https://ge.xhamster.com/videos/das-verbotene-tagebuch-der-monster-titten-9036199", + "review": 0, + "should_download": 0, + "title": "Das Verbotene Tagebuch Der Monster Titten: Free Porn a9 | xHamster", + "file_name": "Das verbotene Tagebuch der Monster Titten [9036199].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3415ace5-9945-41c1-8fea-17bf6c85fe0f.mp4" + }, + { + "id": "3455df27-241f-4e17-815b-448239d237b7", + "created_date": "2024-07-25 07:29:45.872785", + "last_modified_date": "2024-07-25 07:29:45.872785", + "version": 0, + "url": "https://ge.xhamster.com/videos/bffs-boat-party-of-teen-besties-leads-to-hardcore-pounding-with-massive-cock-xhPlWMx", + "review": 0, + "should_download": 0, + "title": "Bffs, eine Bootsparty von Teenie-Besten f\u00fchrt zu Hardcore-H\u00e4mmern mit massivem Schwanz | xHamster", + "file_name": "Bffs, eine Bootsparty von Teenie-Besten f\u00fchrt zu Hardcore-H\u00e4mmern mit massivem Schwanz [xhPlWMx].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3455df27-241f-4e17-815b-448239d237b7.mp4" + }, + { + "id": "34572338-1f9b-4b24-85bf-04676a92d7b6", + "created_date": "2024-07-25 07:29:45.094409", + "last_modified_date": "2024-07-25 07:29:45.094409", + "version": 0, + "url": "https://ge.xhamster.com/videos/it-just-slipped-in-s2-e8-xhWZ1Bd", + "review": 0, + "should_download": 0, + "title": "Es ist gerade reingelegt - s2: e8 | xHamster", + "file_name": "Es ist gerade reingelegt - s2\uff1a e8 [xhWZ1Bd].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/34572338-1f9b-4b24-85bf-04676a92d7b6.mp4" + }, + { + "id": "345d9bb2-dd9a-4e4b-86f9-3dab5af33c9c", + "created_date": "2024-07-25 07:29:46.557202", + "last_modified_date": "2024-07-25 07:29:46.557202", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepmom-to-stepdaughters-bf-i-think-i-can-help-you-with-that-problem-youve-been-having-xhgVX4o", + "review": 0, + "should_download": 0, + "title": "Stiefmutter beim freund der stieftochter \"Ich denke, ich kann dir mit diesem problem helfen, das du hattest\" | xHamster", + "file_name": "Stiefmutter beim freund der stieftochter \uff02Ich denke, ich kann dir mit diesem problem helfen, das du hattest\uff02 [xhgVX4o].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/345d9bb2-dd9a-4e4b-86f9-3dab5af33c9c.mp4" + }, + { + "id": "346fbebb-6cfc-41ad-b75a-66ae849ca54f", + "created_date": "2024-12-29 23:53:27.921639", + "last_modified_date": "2024-12-29 23:53:27.921639", + "version": 0, + "url": "https://ge.xhamster.com/videos/fucking-with-my-step-brother-s6-e2-xhV9Ifw", + "review": 0, + "should_download": 0, + "title": "Ficken mit meinem stiefbruder - S6: e2 | xHamster", + "file_name": "Ficken mit meinem stiefbruder - S6\uff1a e2 [xhV9Ifw].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/346fbebb-6cfc-41ad-b75a-66ae849ca54f.mp4" + }, + { + "id": "3499db1e-bc99-4b43-931d-62de9129d6f6", + "created_date": "2024-07-25 07:29:45.052056", + "last_modified_date": "2024-07-25 07:29:45.052056", + "version": 0, + "url": "https://ge.xhamster.com/videos/loch-um-loch-der-italienische-stecher-1984-14135545", + "review": 0, + "should_download": 0, + "title": "Loch Um Loch - Der Italienische Stecher 1984: Free Porn b0 | xHamster", + "file_name": "Loch um Loch - Der italienische Stecher (1984) [14135545].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/3499db1e-bc99-4b43-931d-62de9129d6f6.mp4" + }, + { + "id": "34c6feb5-3278-4abe-abc6-e4cd402038c4", + "created_date": "2024-07-25 07:29:48.013923", + "last_modified_date": "2024-07-25 07:29:48.013923", + "version": 0, + "url": "https://ge.xhamster.com/videos/teeny-lovers-orgasm-from-double-team-fuck-3755040", + "review": 0, + "should_download": 0, + "title": "Teenie-Liebhaber - Orgasmus vom Doppel-Team-Fick | xHamster", + "file_name": "Teenie-Liebhaber - Orgasmus vom Doppel-Team-Fick [3755040].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/34c6feb5-3278-4abe-abc6-e4cd402038c4.mp4" + }, + { + "id": "34ce1e1c-9273-40cb-98a9-bdcb3da5dd07", + "created_date": "2024-12-29 23:53:27.909583", + "last_modified_date": "2024-12-29 23:53:27.909583", + "version": 0, + "url": "https://ge.xhamster.com/videos/amateur-threesome-with-blonde-while-hubby-films-xhLEgYo", + "review": 0, + "should_download": 0, + "title": "Amateur-Dreier mit Blondine, w\u00e4hrend Ehemann filmt | xHamster", + "file_name": "Amateur-Dreier mit Blondine, w\u00e4hrend Ehemann filmt [xhLEgYo].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/34ce1e1c-9273-40cb-98a9-bdcb3da5dd07.mp4" + }, + { + "id": "35e43363-5102-4c17-a14d-867a3e34252d", + "created_date": "2024-07-25 07:29:45.605830", + "last_modified_date": "2024-07-25 07:29:45.605830", + "version": 0, + "url": "https://ge.xhamster.com/videos/two-married-couples-watch-movies-and-fuck-xh41y6s", + "review": 0, + "should_download": 0, + "title": "Zwei verheiratete Paare gucken Filme und ficken | xHamster", + "file_name": "Zwei verheiratete Paare gucken Filme und ficken [xh41y6s].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/35e43363-5102-4c17-a14d-867a3e34252d.mp4" + }, + { + "id": "363580ef-7ed9-4d84-8e1d-d105acb68e56", + "created_date": "2024-09-05 00:00:22.224000", + "last_modified_date": "2024-10-21 16:26:50.575000", + "version": 1, + "url": "https://ge.xhamster.com/videos/keep-it-in-the-family-12630000", + "review": 0, + "should_download": 0, + "title": "Behalte es in der Familie | xHamster", + "file_name": "Behalte es in der Familie [12630000].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/363580ef-7ed9-4d84-8e1d-d105acb68e56.mp4" + }, + { + "id": "3681f0e4-4825-41d1-8797-6c1e99d0f0c5", + "created_date": "2024-07-25 07:29:45.927013", + "last_modified_date": "2024-07-25 07:29:45.927013", + "version": 0, + "url": "https://ge.xhamster.com/videos/porno-villa-full-german-movie-xhh02ir", + "review": 0, + "should_download": 0, + "title": "Porno Villa - kompletter deutscher Film | xHamster", + "file_name": "Porno Villa - kompletter deutscher Film [xhh02ir].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3681f0e4-4825-41d1-8797-6c1e99d0f0c5.mp4" + }, + { + "id": "36a9b1a9-34fb-471f-9c81-7e3a6a846222", + "created_date": "2024-07-25 07:29:45.725798", + "last_modified_date": "2024-07-25 07:29:45.725798", + "version": 0, + "url": "https://ge.xhamster.com/videos/11-days-11-nights-the-house-of-pleasure-5999494", + "review": 0, + "should_download": 0, + "title": "11 Tage 11 N\u00e4chte (das Haus der Freude) | xHamster", + "file_name": "11 Tage 11 N\u00e4chte (das Haus der Freude) [5999494].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/36a9b1a9-34fb-471f-9c81-7e3a6a846222.mp4" + }, + { + "id": "36d5c16a-db94-401a-90dd-07eddd260da6", + "created_date": "2024-07-25 07:29:46.220620", + "last_modified_date": "2024-07-25 07:29:46.220620", + "version": 0, + "url": "https://ge.xhamster.com/videos/various-artist-18-and-confused-4-xhbpKdN", + "review": 0, + "should_download": 0, + "title": "Verschiedene K\u00fcnstler - 18 und verwirrt 4 | xHamster", + "file_name": "Verschiedene K\u00fcnstler - 18 und verwirrt 4 [xhbpKdN].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/36d5c16a-db94-401a-90dd-07eddd260da6.mp4" + }, + { + "id": "37017fa9-55e0-422f-bb29-50bc655059b2", + "created_date": "2025-01-16 20:33:51.011542", + "last_modified_date": "2025-01-16 20:51:32.550000", + "version": 4, + "url": "https://ge.xhamster.com/videos/never-sieep-alone-720-1984-8710064", + "review": 0, + "should_download": 0, + "title": "Nie sieb alleine 720 - 1984 | xHamster", + "file_name": "Nie sieb alleine 720 - 1984 [8710064].mp4", + "path": null, + "cloud_link": "/data/media/37017fa9-55e0-422f-bb29-50bc655059b2.mp4" + }, + { + "id": "376d59c5-007b-45a5-b197-dc911c5399b5", + "created_date": "2024-07-25 07:29:44.835625", + "last_modified_date": "2024-07-25 07:29:44.835625", + "version": 0, + "url": "https://ge.xhamster.com/videos/neighbors-wife-fucks-to-pay-for-husbands-lost-bet-xhxTXB6", + "review": 0, + "should_download": 0, + "title": "Die Frau des Nachbarn fickt, um die verlorene Wette ihres Mannes zu bezahlen | xHamster", + "file_name": "Die Frau des Nachbarn fickt, um die verlorene Wette ihres Mannes zu bezahlen [xhxTXB6].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/376d59c5-007b-45a5-b197-dc911c5399b5.mp4" + }, + { + "id": "379cc12a-c764-40cf-a7dd-1450bcc02756", + "created_date": "2024-07-25 07:29:44.977944", + "last_modified_date": "2024-07-25 07:29:44.977944", + "version": 0, + "url": "https://ge.xhamster.com/videos/blonde-german-secretary-gets-fucked-by-two-loaded-cocks-xhHt9cX", + "review": 0, + "should_download": 0, + "title": "Die blonde deutsche Sekret\u00e4rin wird von zwei geladenen Schw\u00e4nzen gefickt | xHamster", + "file_name": "Die blonde deutsche Sekret\u00e4rin wird von zwei geladenen Schw\u00e4nzen gefickt [xhHt9cX].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/379cc12a-c764-40cf-a7dd-1450bcc02756.mp4" + }, + { + "id": "37cca3ef-6db7-4221-8f46-293e6bdc1f9e", + "created_date": "2024-11-10 16:53:33.491022", + "last_modified_date": "2024-11-10 16:53:33.491022", + "version": 0, + "url": "https://ge.xhamster.com/videos/please-fuck-me-1996-8702153", + "review": 0, + "should_download": 0, + "title": "Bitte fick mich (1996) | xHamster", + "file_name": "Bitte fick mich (1996) [8702153].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/37cca3ef-6db7-4221-8f46-293e6bdc1f9e.mp4" + }, + { + "id": "37dff553-5957-45dd-ba82-6a9a199953f9", + "created_date": "2024-07-25 07:29:46.648784", + "last_modified_date": "2024-07-25 07:29:46.648784", + "version": 0, + "url": "https://ge.xhamster.com/videos/three-men-in-a-boat-to-say-nothing-of-a-pick-up-girl-scene-4800873", + "review": 0, + "should_download": 0, + "title": "Drei M\u00e4nner in einer Szene in einem Boot (ganz zu schweigen von einem Abholm\u00e4dchen) | xHamster", + "file_name": "Drei M\u00e4nner in einer Szene in einem Boot (ganz zu schweigen von einem Abholm\u00e4dchen) [4800873].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/37dff553-5957-45dd-ba82-6a9a199953f9.mp4" + }, + { + "id": "37f1e6c1-72a2-4621-b0bb-3de377e4d2e2", + "created_date": "2024-07-25 07:29:46.004817", + "last_modified_date": "2024-07-25 07:29:46.004817", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsis-says-you-want-me-to-fuck-my-stepbrother-xhp7gw3", + "review": 0, + "should_download": 0, + "title": "Stiefschwester sagt, du willst, dass ich meinen Stiefbruder ficke ?! | xHamster", + "file_name": "Stiefschwester sagt, du willst, dass ich meinen Stiefbruder ficke \uff1f! [xhp7gw3].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/37f1e6c1-72a2-4621-b0bb-3de377e4d2e2.mp4" + }, + { + "id": "37ff833a-1d49-438b-8012-44edf1ed8034", + "created_date": "2024-07-25 07:29:44.334994", + "last_modified_date": "2024-07-25 07:29:44.334994", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-new-year-s-resolution-is-to-get-creampied-xh8UWCd", + "review": 0, + "should_download": 0, + "title": "Mein Vorsatz f\u00fcrs neue Jahr ist, vollgespritzt zu werden | xHamster", + "file_name": "Mein Vorsatz f\u00fcrs neue Jahr ist, vollgespritzt zu werden [xh8UWCd].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/37ff833a-1d49-438b-8012-44edf1ed8034.mp4" + }, + { + "id": "384af2f6-2345-43e1-9bd6-0123b1f2882a", + "created_date": "2024-10-07 20:47:56.424739", + "last_modified_date": "2024-10-21 16:26:58.757000", + "version": 1, + "url": "https://ge.xhamster.com/videos/great-sex-with-a-virgin-boy-was-a-brat-became-a-man-xhso9QV", + "review": 0, + "should_download": 0, + "title": "Toller Sex mit einem jungfr\u00e4ulichen Jungen. war ein Balg, wurde ein Mann. | xHamster", + "file_name": "Toller Sex mit einem jungfr\u00e4ulichen Jungen. war ein Balg, wurde ein Mann. [xhso9QV].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/384af2f6-2345-43e1-9bd6-0123b1f2882a.mp4" + }, + { + "id": "388cee3b-976a-4c7c-b399-a8a3ddbc3598", + "created_date": "2025-01-16 19:59:58.549449", + "last_modified_date": "2025-01-16 19:59:58.549468", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-best-1-xhNv8Y4", + "review": 0, + "should_download": 0, + "title": "Das Beste 1 | xHamster", + "file_name": "Das Beste 1 [xhNv8Y4].mp4", + "path": null, + "cloud_link": "/data/media/388cee3b-976a-4c7c-b399-a8a3ddbc3598.mp4" + }, + { + "id": "389dd21e-ff53-4f6b-9442-b4cae676b3e2", + "created_date": "2024-07-25 07:29:46.249460", + "last_modified_date": "2024-07-25 07:29:46.249460", + "version": 0, + "url": "https://ge.xhamster.com/videos/tiny-hot-brunette-brooklyn-gray-opens-her-ass-cheeks-and-gets-fucked-xhk2TEc", + "review": 0, + "should_download": 0, + "title": "Die kleine hei\u00dfe Br\u00fcnette Brooklyn Grey \u00f6ffnet ihre Arschbacken und wird gefickt | xHamster", + "file_name": "Die kleine hei\u00dfe Br\u00fcnette Brooklyn Grey \u00f6ffnet ihre Arschbacken und wird gefickt [xhk2TEc].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/389dd21e-ff53-4f6b-9442-b4cae676b3e2.mp4" + }, + { + "id": "38f38ba9-708b-4d1c-a0bb-7c9f41ee8829", + "created_date": "2024-07-25 07:29:45.876368", + "last_modified_date": "2024-07-25 07:29:45.876368", + "version": 0, + "url": "https://ge.xhamster.com/videos/gang-bang-my-wife-scene-07-xhYxImd", + "review": 0, + "should_download": 0, + "title": "Gangbang mit meiner Ehefrau - Szene # 07 | xHamster", + "file_name": "Gangbang mit meiner Ehefrau - Szene # 07 [xhYxImd].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/38f38ba9-708b-4d1c-a0bb-7c9f41ee8829.mp4" + }, + { + "id": "390e7fd9-ced2-4e6e-9758-cfbd2adb0683", + "created_date": "2024-07-25 07:29:46.441817", + "last_modified_date": "2024-07-25 07:29:46.441817", + "version": 0, + "url": "https://ge.xhamster.com/videos/exxxtra-small-tiny-teen-fucked-by-her-swimming-coach-xhAZL8H", + "review": 0, + "should_download": 0, + "title": "Exxxtra small - kleines Teen von ihrem Schwimmtrainer gefickt | xHamster", + "file_name": "Exxxtra small - kleines Teen von ihrem Schwimmtrainer gefickt [xhAZL8H].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/390e7fd9-ced2-4e6e-9758-cfbd2adb0683.mp4" + }, + { + "id": "3911c5f4-c45b-4692-b141-ec71e52b57ec", + "created_date": "2025-01-16 19:59:55.961303", + "last_modified_date": "2025-01-16 19:59:55.961309", + "version": 0, + "url": "https://ge.xhamster.com/videos/babes-get-their-pussy-and-ass-fucked-in-hard-group-sex-xhFgn95", + "review": 0, + "should_download": 0, + "title": "Sch\u00e4tzchen bekommen ihre muschi und ihren arsch beim harten gruppensex gefickt | xHamster", + "file_name": "Sch\u00e4tzchen bekommen ihre muschi und ihren arsch beim harten gruppensex gefickt [xhFgn95].mp4", + "path": null, + "cloud_link": "/data/media/3911c5f4-c45b-4692-b141-ec71e52b57ec.mp4" + }, + { + "id": "3939ec81-409e-46d3-b3c4-01c161604b03", + "created_date": "2024-12-29 23:53:27.888386", + "last_modified_date": "2024-12-29 23:53:27.888386", + "version": 0, + "url": "https://ge.xhamster.com/videos/mad-max-04-chapter-01-xhtJOIu", + "review": 0, + "should_download": 0, + "title": "Mad Max # 04 - Kapitel # 01 | xHamster", + "file_name": "Mad Max # 04 - Kapitel # 01 [xhtJOIu].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/3939ec81-409e-46d3-b3c4-01c161604b03.mp4" + }, + { + "id": "393f491d-6239-4a68-b8d7-00ac984ed197", + "created_date": "2025-01-16 19:59:47.081277", + "last_modified_date": "2025-01-16 19:59:47.081284", + "version": 0, + "url": "https://ge.xhamster.com/videos/momsteachsex-horny-step-mom-tricks-teen-into-hot-threeway-7413429", + "review": 0, + "should_download": 0, + "title": "Momsteachsex - eine geile Stiefmutter trickst Teen in hei\u00dfen Dreier aus | xHamster", + "file_name": "Momsteachsex - eine geile Stiefmutter trickst Teen in hei\u00dfen Dreier aus [7413429].mp4", + "path": null, + "cloud_link": "/data/media/393f491d-6239-4a68-b8d7-00ac984ed197.mp4" + }, + { + "id": "3983b1d6-3e2a-431d-a796-4d7e2c65fffb", + "created_date": "2024-07-25 07:29:45.426324", + "last_modified_date": "2024-07-25 07:29:45.426324", + "version": 0, + "url": "https://ge.xhamster.com/videos/pure-taboo-step-parents-step-bro-welcome-new-sister-11813904", + "review": 0, + "should_download": 0, + "title": "Reines Tabu, Stiefeltern & Stiefbruder begr\u00fc\u00dfen neue Schwester | xHamster", + "file_name": "Reines Tabu, Stiefeltern & Stiefbruder begr\u00fc\u00dfen neue Schwester [11813904].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3983b1d6-3e2a-431d-a796-4d7e2c65fffb.mp4" + }, + { + "id": "399df10f-316c-466c-bac4-58dfe257b959", + "created_date": "2024-10-07 20:47:56.408036", + "last_modified_date": "2024-10-21 16:27:04.212000", + "version": 1, + "url": "https://ge.xhamster.com/videos/debuttante-35-2833809", + "review": 0, + "should_download": 0, + "title": "Debuttante 35: Free Vintage Porn Video 9f | xHamster", + "file_name": "Debuttante 35 [2833809].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/399df10f-316c-466c-bac4-58dfe257b959.mp4" + }, + { + "id": "39aaef9d-2374-4d8d-bf06-3fcfe09e3fa3", + "created_date": "2024-07-25 07:29:47.364362", + "last_modified_date": "2024-07-25 07:29:47.364362", + "version": 0, + "url": "https://ge.xhamster.com/videos/i-fucked-my-step-brother-accidentally-on-purpose-s18-e9-xhPXuax", + "review": 0, + "should_download": 0, + "title": "Ich habe meinen stiefbruer versehentlich zu einem zweck gefickt - s18: e9 | xHamster", + "file_name": "Ich habe meinen stiefbruer versehentlich zu einem zweck gefickt - s18\uff1a e9 [xhPXuax].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/39aaef9d-2374-4d8d-bf06-3fcfe09e3fa3.mp4" + }, + { + "id": "39af1a11-2632-4186-b5b9-e6ab11b9f92d", + "created_date": "2024-07-25 07:29:45.912341", + "last_modified_date": "2024-07-25 07:29:45.912341", + "version": 0, + "url": "https://ge.xhamster.com/videos/meridian-in-bed-3-guys-1552452", + "review": 0, + "should_download": 0, + "title": "Meridian im Bett & 3 Typen | xHamster", + "file_name": "Meridian im Bett & 3 Typen [1552452].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/39af1a11-2632-4186-b5b9-e6ab11b9f92d.mp4" + }, + { + "id": "39b253a0-7182-4da4-82d7-4e1759493b00", + "created_date": "2024-10-21 15:08:43.546659", + "last_modified_date": "2024-10-21 16:27:13.177000", + "version": 1, + "url": "https://ge.xhamster.com/videos/slutty-brunet-has-two-guests-over-for-dinner-then-has-kinky-threesome-1518485", + "review": 0, + "should_download": 0, + "title": "Die versaute Br\u00fcnette hat zwei G\u00e4ste zum Abendessen und hat dann einen versauten Dreier | xHamster", + "file_name": "Die versaute Br\u00fcnette hat zwei G\u00e4ste zum Abendessen und hat dann einen versauten Dreier [1518485].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/39b253a0-7182-4da4-82d7-4e1759493b00.mp4" + }, + { + "id": "39dce106-a695-44f7-96c9-5fd532066bb1", + "created_date": "2024-07-25 07:29:46.073867", + "last_modified_date": "2024-07-25 07:29:46.073867", + "version": 0, + "url": "https://ge.xhamster.com/videos/he-is-a-real-pig-xh0u0DH", + "review": 0, + "should_download": 0, + "title": "Er ist ein echtes Schwein | xHamster", + "file_name": "Er ist ein echtes Schwein [xh0u0DH].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/39dce106-a695-44f7-96c9-5fd532066bb1.mp4" + }, + { + "id": "3a3e4635-a06c-4ea4-9224-d90acdcdd463", + "created_date": "2024-07-25 07:29:47.134149", + "last_modified_date": "2024-07-25 07:29:47.134149", + "version": 0, + "url": "https://ge.xhamster.com/videos/familien-feier-endet-im-gangbang-xhhg5OR", + "review": 0, + "should_download": 0, + "title": "Familien Feier Endet Im Gangbang, Free HD Porn f5 | xHamster", + "file_name": "Familien feier endet im Gangbang [xhhg5OR].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3a3e4635-a06c-4ea4-9224-d90acdcdd463.mp4" + }, + { + "id": "3a4a9697-6575-40db-b8dc-500deb86d13e", + "created_date": "2024-07-25 07:29:45.259849", + "last_modified_date": "2024-07-25 07:29:45.259849", + "version": 0, + "url": "https://ge.xhamster.com/videos/sex-addict-secretary-fucks-a-colleague-in-front-of-her-boss-12942177", + "review": 0, + "should_download": 0, + "title": "Sexs\u00fcchtige Sekret\u00e4rin fickt eine Kollegin vor ihrem Chef | xHamster", + "file_name": "Sexs\u00fcchtige Sekret\u00e4rin fickt eine Kollegin vor ihrem Chef [12942177].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3a4a9697-6575-40db-b8dc-500deb86d13e.mp4" + }, + { + "id": "3aa4c807-b1fb-459e-9603-59b6f8cd51de", + "created_date": "2024-07-25 07:29:46.301871", + "last_modified_date": "2024-07-25 07:29:46.301871", + "version": 0, + "url": "https://ge.xhamster.com/videos/snow-white-and-7-dwarfs-1995-6305400", + "review": 0, + "should_download": 0, + "title": "Schneewittchen und 7 Zwerge (1995) | xHamster", + "file_name": "Schneewittchen und 7 Zwerge (1995) [6305400].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3aa4c807-b1fb-459e-9603-59b6f8cd51de.mp4" + }, + { + "id": "3aa9c2a0-b6ed-4d2c-ab6c-13671393fcf4", + "created_date": "2025-01-17 16:34:39.440015", + "last_modified_date": "2025-01-17 22:49:37.411000", + "version": 1, + "url": "https://ge.xhamster.com/videos/swap-your-daughter-compilation-xhsMRtd", + "review": 0, + "should_download": 0, + "title": "Tausche deine Tochter-Zusammenstellung | xHamster", + "file_name": "Tausche deine Tochter-Zusammenstellung [xhsMRtd].mp4", + "path": null, + "cloud_link": "/data/media/3aa9c2a0-b6ed-4d2c-ab6c-13671393fcf4.mp4" + }, + { + "id": "3aac90dc-962c-447d-a268-6e88f49935ce", + "created_date": "2024-07-25 07:29:46.378927", + "last_modified_date": "2024-07-25 07:29:46.378927", + "version": 0, + "url": "https://ge.xhamster.com/videos/various-artist-18-and-confused-7-xhnE4DF", + "review": 0, + "should_download": 0, + "title": "Verschiedene K\u00fcnstler - 18 und verwirrt 7 | xHamster", + "file_name": "Verschiedene K\u00fcnstler - 18 und verwirrt 7 [xhnE4DF].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3aac90dc-962c-447d-a268-6e88f49935ce.mp4" + }, + { + "id": "3abe034e-2b9b-4a5d-8a42-8b0afbe7f843", + "created_date": "2024-10-07 20:47:56.426165", + "last_modified_date": "2024-10-21 16:27:19.358000", + "version": 1, + "url": "https://ge.xhamster.com/videos/our-german-big-family-part-01-xh6mzil", + "review": 0, + "should_download": 0, + "title": "Unsere deutsche gro\u00dfe Familie !!! - Teil # 01 | xHamster", + "file_name": "Unsere deutsche gro\u00dfe Familie !!! - Teil # 01 [xh6mzil].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/3abe034e-2b9b-4a5d-8a42-8b0afbe7f843.mp4" + }, + { + "id": "3afa5947-5ecd-44e8-8823-ea5fcb2ce7aa", + "created_date": "2024-07-25 07:29:45.344148", + "last_modified_date": "2024-07-25 07:29:45.344148", + "version": 0, + "url": "https://ge.xhamster.com/videos/missax-i-did-this-for-you-charlie-forde-xhN4mIi", + "review": 0, + "should_download": 0, + "title": "Missax - ich habe das f\u00fcr dich gemacht - charlie forde | xHamster", + "file_name": "Missax - ich habe das f\u00fcr dich gemacht - charlie forde [xhN4mIi].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/3afa5947-5ecd-44e8-8823-ea5fcb2ce7aa.mp4" + }, + { + "id": "3b175348-c4dc-46b8-8e89-ac0e477f61ec", + "created_date": "2024-08-28 23:21:54.376200", + "last_modified_date": "2024-08-28 23:21:54.376200", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-roommates-ex-girlfriend-asked-me-for-a-facial-xh0b0kp", + "review": 0, + "should_download": 0, + "title": "Ex-freundin meines mitbewohners bat mich um eine gesichtsbesamung | xHamster", + "file_name": "Ex-freundin meines mitbewohners bat mich um eine gesichtsbesamung [xh0b0kp].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/3b175348-c4dc-46b8-8e89-ac0e477f61ec.mp4" + }, + { + "id": "3b256888-4e61-4a7b-b215-ef380d4ab71a", + "created_date": "2024-11-10 16:53:33.497640", + "last_modified_date": "2024-11-10 16:53:33.497640", + "version": 0, + "url": "https://ge.xhamster.com/videos/group-15-xhurxjF", + "review": 0, + "should_download": 0, + "title": "Gruppe 15 | xHamster", + "file_name": "Gruppe 15 [xhurxjF].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/3b256888-4e61-4a7b-b215-ef380d4ab71a.mp4" + }, + { + "id": "3b539f74-5d52-452f-aa83-33e88f5463a2", + "created_date": "2024-09-24 08:11:39.002314", + "last_modified_date": "2024-10-21 16:27:25.417000", + "version": 1, + "url": "https://ge.xhamster.com/videos/camping-with-a-nudist-girlfriend-compilation-xhTiwRM", + "review": 0, + "should_download": 0, + "title": "Camping mit einer fkk-freundin - zusammenstellung | xHamster", + "file_name": "Camping mit einer fkk-freundin - zusammenstellung [xhTiwRM].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/3b539f74-5d52-452f-aa83-33e88f5463a2.mp4" + }, + { + "id": "3b9a0c12-bf8c-4915-a3b9-b26830cfe5e2", + "created_date": "2024-10-21 15:08:43.545947", + "last_modified_date": "2024-10-21 16:27:29.928000", + "version": 1, + "url": "https://ge.xhamster.com/videos/caught-by-friend-with-stepbrother-blonde-teen-slut-gets-2-cocks-shoved-in-xhqmuTX", + "review": 0, + "should_download": 0, + "title": "Vom freund mit dem Stiefbruder erwischt! blonde teen schlampe bekommt 2 schwaenze rein geschoben! | xHamster", + "file_name": "Vom freund mit dem Stiefbruder erwischt! blonde teen schlampe bekommt 2 schwaenze rein geschoben! [xhqmuTX].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/3b9a0c12-bf8c-4915-a3b9-b26830cfe5e2.mp4" + }, + { + "id": "3bc516ed-cb7e-45bc-9137-57f4812131a4", + "created_date": "2024-07-25 07:29:46.892583", + "last_modified_date": "2024-07-25 07:29:46.892583", + "version": 0, + "url": "https://ge.xhamster.com/videos/la-locanda-della-maladolescenza-1980-xhkdAJh", + "review": 0, + "should_download": 0, + "title": "Das Gasthaus zur Wollust (1980) | xHamster", + "file_name": "Das Gasthaus zur Wollust (1980) [xhkdAJh].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3bc516ed-cb7e-45bc-9137-57f4812131a4.mp4" + }, + { + "id": "3c1002b0-7a5a-490c-9937-5c0b55f124e6", + "created_date": "2024-07-25 07:29:47.005079", + "last_modified_date": "2024-07-25 07:29:47.005079", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-fucking-14435642", + "review": 0, + "should_download": 0, + "title": "Familienfick | xHamster", + "file_name": "Familienfick [14435642].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3c1002b0-7a5a-490c-9937-5c0b55f124e6.mp4" + }, + { + "id": "3c438c76-6c79-407e-bf68-b78b984baf6f", + "created_date": "2024-08-09 20:20:00.727947", + "last_modified_date": "2024-08-16 10:29:46.814000", + "version": 1, + "url": "https://ge.xhamster.com/videos/strip-spin-the-bottle-with-bella-angel-kitty-kat-medison-stanley-xhoUAHX", + "review": 0, + "should_download": 0, + "title": "Strip-Spin die Flasche mit bella angel, Kitty Kat, Medison & Stanley | xHamster", + "file_name": "Strip-Spin die Flasche mit bella angel, Kitty Kat, Medison & Stanley [xhoUAHX].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/3c438c76-6c79-407e-bf68-b78b984baf6f.mp4" + }, + { + "id": "3c4de77a-f465-4b6c-ac34-f2cee5b2a76a", + "created_date": "2024-07-25 07:29:46.065119", + "last_modified_date": "2024-07-25 07:29:46.065119", + "version": 0, + "url": "https://ge.xhamster.com/videos/very-good-orgy-of-random-age-2268258", + "review": 0, + "should_download": 0, + "title": "Sehr gute Orgie zuf\u00e4lligen Alters! | xHamster", + "file_name": "Sehr gute Orgie zuf\u00e4lligen Alters! [2268258].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3c4de77a-f465-4b6c-ac34-f2cee5b2a76a.mp4" + }, + { + "id": "3c91e953-25cc-41fa-ac0b-3a7d2bb95635", + "created_date": "2024-12-29 23:53:27.881894", + "last_modified_date": "2024-12-29 23:53:27.881894", + "version": 0, + "url": "https://ge.xhamster.com/videos/clumsy-secretary-gets-fucked-like-a-slut-instead-of-being-fired-xh3os6H", + "review": 0, + "should_download": 0, + "title": "Ungeschickte Sekret\u00e4rin wird wie eine Schlampe gefickt, anstatt gefeuert zu werden! | xHamster", + "file_name": "Ungeschickte Sekret\u00e4rin wird wie eine Schlampe gefickt, anstatt gefeuert zu werden! [xh3os6H].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/3c91e953-25cc-41fa-ac0b-3a7d2bb95635.mp4" + }, + { + "id": "3cbbc7ac-ae4f-4d86-8337-1376d085f50d", + "created_date": "2024-07-25 07:29:46.997750", + "last_modified_date": "2024-07-25 07:29:46.997750", + "version": 0, + "url": "https://ge.xhamster.com/videos/shameless-boat-ride-summer-vibes-xhyKHkn", + "review": 0, + "should_download": 0, + "title": "Schamlose Schifffahrt, Sommerstimmung | xHamster", + "file_name": "Schamlose Schifffahrt, Sommerstimmung [xhyKHkn].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3cbbc7ac-ae4f-4d86-8337-1376d085f50d.mp4" + }, + { + "id": "3ce0c676-a48a-4a11-96ab-8bac42a0b284", + "created_date": "2024-07-25 07:29:46.655876", + "last_modified_date": "2024-07-25 07:29:46.655876", + "version": 0, + "url": "https://ge.xhamster.com/videos/school-excess-1996-12377529", + "review": 0, + "should_download": 0, + "title": "Schulexzess 1996 | xHamster", + "file_name": "Schulexzess 1996 [12377529].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3ce0c676-a48a-4a11-96ab-8bac42a0b284.mp4" + }, + { + "id": "3d1e7c20-3721-490e-bf60-25c14ba58fbc", + "created_date": "2024-07-25 07:29:48.172689", + "last_modified_date": "2024-07-25 07:29:48.172689", + "version": 0, + "url": "https://ge.xhamster.com/videos/husband-shares-wife-with-friend-in-threesome-1-part-xhsDq0u", + "review": 0, + "should_download": 0, + "title": "Ehemann teilt Ehefrau mit Freund zu dritt - 1 Teil | xHamster", + "file_name": "Ehemann teilt Ehefrau mit Freund zu dritt - 1 Teil [xhsDq0u].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3d1e7c20-3721-490e-bf60-25c14ba58fbc.mp4" + }, + { + "id": "3d735197-23fb-4fdb-9605-98064160e74d", + "created_date": "2024-07-25 07:29:47.202134", + "last_modified_date": "2024-07-25 07:29:47.202134", + "version": 0, + "url": "https://ge.xhamster.com/videos/cutie-fucking-a-stranger-at-the-beach-3033686", + "review": 0, + "should_download": 0, + "title": "S\u00fc\u00dfe, fickt einen Fremden am Strand | xHamster", + "file_name": "S\u00fc\u00dfe, fickt einen Fremden am Strand [3033686].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3d735197-23fb-4fdb-9605-98064160e74d.mp4" + }, + { + "id": "3de00a28-3332-443a-896e-c61e20c1e4a1", + "created_date": "2024-08-09 21:28:29.409701", + "last_modified_date": "2024-08-16 10:30:01.095000", + "version": 1, + "url": "https://ge.xhamster.com/videos/my-roommates-girlfriend-helps-me-relax-after-work-and-gets-a-huge-facial-xhwmpxJ", + "review": 0, + "should_download": 0, + "title": "Die freundin meines mitbewohners hilft mir, mich nach der arbeit zu entspannen und bekommt eine riesige gesichtsbesamung | xHamster", + "file_name": "Die freundin meines mitbewohners hilft mir, mich nach der arbeit zu entspannen und bekommt eine riesige gesichtsbesamung [xhwmpxJ].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/3de00a28-3332-443a-896e-c61e20c1e4a1.mp4" + }, + { + "id": "3e7b5f72-9f40-44d0-9322-43febb7f20d0", + "created_date": "2024-07-25 07:29:45.729331", + "last_modified_date": "2024-07-25 07:29:45.729331", + "version": 0, + "url": "https://ge.xhamster.com/videos/auf-der-couch-xhdTVv0", + "review": 0, + "should_download": 0, + "title": "Auf Der Couch: Free Porn Video e9 | xHamster", + "file_name": "Auf der Couch [xhdTVv0].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3e7b5f72-9f40-44d0-9322-43febb7f20d0.mp4" + }, + { + "id": "3ee7847b-d9ce-4f09-9287-9ef450ad4e03", + "created_date": "2024-07-25 07:29:46.465628", + "last_modified_date": "2024-07-25 07:29:46.465628", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-inc-2-full-german-movie-xhNzSgC", + "review": 0, + "should_download": 0, + "title": "Familie inkl. 2 - kompletter deutscher Film | xHamster", + "file_name": "Familie inkl. 2 - kompletter deutscher Film [xhNzSgC].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3ee7847b-d9ce-4f09-9287-9ef450ad4e03.mp4" + }, + { + "id": "3f241ea2-5ece-40c4-855a-c66e34607b83", + "created_date": "2024-11-10 16:53:33.484938", + "last_modified_date": "2024-11-10 16:53:33.484938", + "version": 0, + "url": "https://ge.xhamster.com/videos/apartment-complete-film-original-hd-version-xhXG8xg", + "review": 0, + "should_download": 0, + "title": "Wohnung (kompletter Film - Original HD-Version) | xHamster", + "file_name": "Wohnung (kompletter Film - Original HD-Version) [xhXG8xg].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/3f241ea2-5ece-40c4-855a-c66e34607b83.mp4" + }, + { + "id": "3f62e872-49b9-4f28-a481-b42a8389193a", + "created_date": "2024-07-25 07:29:45.967413", + "last_modified_date": "2024-07-25 07:29:45.967413", + "version": 0, + "url": "https://ge.xhamster.com/videos/swapsis-says-is-that-your-dick-xhqs0jZ", + "review": 0, + "should_download": 0, + "title": "Stiefschwester sagt, ist das dein Schwanz ?! | xHamster", + "file_name": "Stiefschwester sagt, ist das dein Schwanz \uff1f! [xhqs0jZ].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3f62e872-49b9-4f28-a481-b42a8389193a.mp4" + }, + { + "id": "3f63a803-fcb5-4e8d-a1a7-81d4a62c59be", + "created_date": "2024-07-25 07:29:44.938929", + "last_modified_date": "2024-07-25 07:29:44.938929", + "version": 0, + "url": "https://ge.xhamster.com/videos/barely-legal-16-2001-xh4qPHp", + "review": 0, + "should_download": 0, + "title": "Barely Legal 16 (2001) | xHamster", + "file_name": "Barely Legal 16 (2001) [xh4qPHp].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3f63a803-fcb5-4e8d-a1a7-81d4a62c59be.mp4" + }, + { + "id": "3f63cb4e-869c-418b-8bc8-e64be7f5fe56", + "created_date": "2024-07-25 07:29:45.774240", + "last_modified_date": "2024-07-25 07:29:45.774240", + "version": 0, + "url": "https://ge.xhamster.com/videos/magical-boat-ride-7217483", + "review": 0, + "should_download": 0, + "title": "Magische Bootsfahrt | xHamster", + "file_name": "Magische Bootsfahrt [7217483].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3f63cb4e-869c-418b-8bc8-e64be7f5fe56.mp4" + }, + { + "id": "3f990a9b-4574-41df-bf3d-73010d969fbc", + "created_date": "2024-07-25 07:29:47.258050", + "last_modified_date": "2024-07-25 07:29:47.258050", + "version": 0, + "url": "https://ge.xhamster.com/videos/bang-bang-yankees-party-in-usa-vol-18-xhipIwY", + "review": 0, + "should_download": 0, + "title": "Bang-Bang-Yankees-Party in den USA !!! - vol. # 18 | xHamster", + "file_name": "Bang-Bang-Yankees-Party in den USA !!! - vol. # 18 [xhipIwY].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3f990a9b-4574-41df-bf3d-73010d969fbc.mp4" + }, + { + "id": "3ffc4d9f-2ca9-4c34-bab0-f479648fcc87", + "created_date": "2024-07-25 07:29:46.126274", + "last_modified_date": "2024-07-25 07:29:46.126274", + "version": 0, + "url": "https://ge.xhamster.com/videos/nurumassage-chanel-preston-helps-young-couple-loosen-up-13426321", + "review": 0, + "should_download": 0, + "title": "Nurumassage Chanel Preston hilft jungem Paar, sich zu lockern | xHamster", + "file_name": "Nurumassage Chanel Preston hilft jungem Paar, sich zu lockern [13426321].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/3ffc4d9f-2ca9-4c34-bab0-f479648fcc87.mp4" + }, + { + "id": "40205e6b-f145-4361-ab22-5545dea1c87e", + "created_date": "2024-07-25 07:29:46.753716", + "last_modified_date": "2024-07-25 07:29:46.753716", + "version": 0, + "url": "https://ge.xhamster.com/videos/your-wife-or-mine-good-quality-7550244", + "review": 0, + "should_download": 0, + "title": "Deine Frau oder meine (gute Qualit\u00e4t) | xHamster", + "file_name": "Deine Frau oder meine (gute Qualit\u00e4t) [7550244].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/40205e6b-f145-4361-ab22-5545dea1c87e.mp4" + }, + { + "id": "409edf04-57c8-45d9-aaa1-fb32e631a51f", + "created_date": "2024-07-25 07:29:46.625934", + "last_modified_date": "2024-07-25 07:29:46.625934", + "version": 0, + "url": "https://ge.xhamster.com/videos/voyeur-and-nudist-session-with-alba-and-a-stranger-10086125", + "review": 0, + "should_download": 0, + "title": "Voyeur- und FKK-Session mit Alba und einem Fremden | xHamster", + "file_name": "Voyeur- und FKK-Session mit Alba und einem Fremden [10086125].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/409edf04-57c8-45d9-aaa1-fb32e631a51f.mp4" + }, + { + "id": "40d24e4b-fea9-4596-a400-4f92cf1b930b", + "created_date": "2024-11-10 16:53:33.471655", + "last_modified_date": "2024-11-10 16:53:33.471655", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-nephew-9030756", + "review": 0, + "should_download": 0, + "title": "Der Neffe | xHamster", + "file_name": "Der Neffe [9030756].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/40d24e4b-fea9-4596-a400-4f92cf1b930b.mp4" + }, + { + "id": "40f57318-189d-46c5-ac53-bf363fa5db24", + "created_date": "2024-07-25 07:29:45.970900", + "last_modified_date": "2024-07-25 07:29:45.970900", + "version": 0, + "url": "https://ge.xhamster.com/videos/18-and-confused-5-xhb2CTW", + "review": 0, + "should_download": 0, + "title": "18 und verwirrt # 5 | xHamster", + "file_name": "18 und verwirrt # 5 [xhb2CTW].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/40f57318-189d-46c5-ac53-bf363fa5db24.mp4" + }, + { + "id": "40fb1151-bba1-4513-b4bc-469af80de9c0", + "created_date": "2024-09-24 08:11:39.002009", + "last_modified_date": "2024-10-21 16:27:35.816000", + "version": 1, + "url": "https://ge.xhamster.com/videos/my-dirty-hobby-hard-threesome-pounding-7525930", + "review": 0, + "should_download": 0, + "title": "Mein schmutziges Hobby, harter Dreier | xHamster", + "file_name": "Mein schmutziges Hobby, harter Dreier [7525930].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/40fb1151-bba1-4513-b4bc-469af80de9c0.mp4" + }, + { + "id": "416a5b49-ba33-4b64-a988-a8029f865ce5", + "created_date": "2024-07-25 07:29:44.647148", + "last_modified_date": "2024-07-25 07:29:44.647148", + "version": 0, + "url": "https://ge.xhamster.com/videos/wife-gets-fucked-on-public-beach-by-husband-and-his-friend-ending-in-double-creampie-xhL79fR", + "review": 0, + "should_download": 0, + "title": "Ehefrau wird am \u00f6ffentlichen Strand von Ehemann und seinem Freund gefickt und endet mit doppeltem Creampie | xHamster", + "file_name": "Ehefrau wird am \u00f6ffentlichen Strand von Ehemann und seinem Freund gefickt und endet mit doppeltem Creampie [xhL79fR].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/416a5b49-ba33-4b64-a988-a8029f865ce5.mp4" + }, + { + "id": "41d6f249-c2db-47d6-a4f6-402d3cd03005", + "created_date": "2024-07-25 07:29:46.204701", + "last_modified_date": "2024-07-25 07:29:46.204701", + "version": 0, + "url": "https://ge.xhamster.com/videos/big-boobs-office-slut-fucks-big-cocked-stud-8481040", + "review": 0, + "should_download": 0, + "title": "Dicke B\u00fcro-Schlampe mit dicken M\u00f6psen fickt gro\u00dfen Schwanz | xHamster", + "file_name": "Dicke B\u00fcro-Schlampe mit dicken M\u00f6psen fickt gro\u00dfen Schwanz [8481040].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/41d6f249-c2db-47d6-a4f6-402d3cd03005.mp4" + }, + { + "id": "422f3652-2b51-4330-ba1e-b8029b8905f1", + "created_date": "2024-07-25 07:29:46.772842", + "last_modified_date": "2024-07-25 07:29:46.772842", + "version": 0, + "url": "https://ge.xhamster.com/videos/vixen-com-hot-babysitter-fucked-by-her-boss-6532667", + "review": 0, + "should_download": 0, + "title": "Vixen.com, hei\u00dfer Babysitter von ihrem Chef gefickt | xHamster", + "file_name": "Vixen.com, hei\u00dfer Babysitter von ihrem Chef gefickt [6532667].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/422f3652-2b51-4330-ba1e-b8029b8905f1.mp4" + }, + { + "id": "4234b172-df47-4cae-b389-8b3b9d4507d5", + "created_date": "2024-07-25 07:29:47.310917", + "last_modified_date": "2024-07-25 07:29:47.310917", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-fun-fantasy-xhJlIqZ", + "review": 0, + "should_download": 0, + "title": "Familienspa\u00df-Fantasie | xHamster", + "file_name": "Familienspa\u00df-Fantasie [xhJlIqZ].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4234b172-df47-4cae-b389-8b3b9d4507d5.mp4" + }, + { + "id": "4235a122-d2ee-405e-a8ae-3194b841311e", + "created_date": "2024-07-25 07:29:44.965154", + "last_modified_date": "2024-07-25 07:29:44.965154", + "version": 0, + "url": "https://ge.xhamster.com/videos/step-sister-likes-to-be-naked-xhS7JBS", + "review": 0, + "should_download": 0, + "title": "Stiefschwester ist gerne nackt | xHamster", + "file_name": "Stiefschwester ist gerne nackt [xhS7JBS].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4235a122-d2ee-405e-a8ae-3194b841311e.mp4" + }, + { + "id": "424328cf-9035-4df1-8cdc-42115d8d51a3", + "created_date": "2024-10-21 15:08:43.549133", + "last_modified_date": "2024-10-21 16:27:46.678000", + "version": 1, + "url": "https://ge.xhamster.com/videos/the-swinging-seventies-threesome-mfm-scene-4445648", + "review": 0, + "should_download": 0, + "title": "The Swinging Seventies (Dreier-MFM-Szene) | xHamster", + "file_name": "The Swinging Seventies (Dreier-MFM-Szene) [4445648].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/424328cf-9035-4df1-8cdc-42115d8d51a3.mp4" + }, + { + "id": "42804d74-c912-4d28-8122-51d331ae5ca1", + "created_date": "2024-07-25 07:29:44.465755", + "last_modified_date": "2024-07-25 07:29:44.465755", + "version": 0, + "url": "https://ge.xhamster.com/videos/have-fun-with-our-neighbors-7742107", + "review": 0, + "should_download": 0, + "title": "Haben Sie Spa\u00df mit unseren Nachbarn | xHamster", + "file_name": "Haben Sie Spa\u00df mit unseren Nachbarn [7742107].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/42804d74-c912-4d28-8122-51d331ae5ca1.mp4" + }, + { + "id": "431312ad-324a-430f-bbd8-26c5daba4ce9", + "created_date": "2024-07-25 07:29:45.951959", + "last_modified_date": "2024-07-25 07:29:45.951959", + "version": 0, + "url": "https://ge.xhamster.com/videos/familie-immerscharf-4-10812927", + "review": 0, + "should_download": 0, + "title": "Familie Immerscharf 4, Free HD Porn Video 66 | xHamster", + "file_name": "Familie Immerscharf 4 [10812927].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/431312ad-324a-430f-bbd8-26c5daba4ce9.mp4" + }, + { + "id": "4330c224-a07f-469c-96e0-cb9dc2d5df34", + "created_date": "2024-07-25 07:29:45.008992", + "last_modified_date": "2024-07-25 07:29:45.008992", + "version": 0, + "url": "https://ge.xhamster.com/videos/a-couple-receives-friends-for-an-orgy-xhwvb3h", + "review": 0, + "should_download": 0, + "title": "Ein Paar empf\u00e4ngt Freunde f\u00fcr eine Orgie | xHamster", + "file_name": "Ein Paar empf\u00e4ngt Freunde f\u00fcr eine Orgie [xhwvb3h].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/4330c224-a07f-469c-96e0-cb9dc2d5df34.mp4" + }, + { + "id": "4342200b-e168-41e6-b536-28c1ff407a9e", + "created_date": "2024-07-25 07:29:44.472957", + "last_modified_date": "2024-07-25 07:29:44.472957", + "version": 0, + "url": "https://ge.xhamster.com/videos/aunt-and-her-cougar-friend-find-nephew-upscaled-to-4k-xhnjYpQ", + "review": 0, + "should_download": 0, + "title": "Tante und ihre MILF-Freundin finden Neffen, auf 4k hochskaliert | xHamster", + "file_name": "Tante und ihre MILF-Freundin finden Neffen, auf 4k hochskaliert [xhnjYpQ].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4342200b-e168-41e6-b536-28c1ff407a9e.mp4" + }, + { + "id": "43464a08-9992-46f0-b6bc-d824c7f0f006", + "created_date": "2024-07-25 07:29:44.639836", + "last_modified_date": "2024-07-25 07:29:44.639836", + "version": 0, + "url": "https://ge.xhamster.com/videos/fucked-on-mallorca-vacation-by-2-guys-ao-10090812", + "review": 0, + "should_download": 0, + "title": "Im Mallorca Urlaub von 2 Kerlen AO Abgefickt | xHamster", + "file_name": "Im Mallorca Urlaub von 2 Kerlen AO Abgefickt [10090812].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/43464a08-9992-46f0-b6bc-d824c7f0f006.mp4" + }, + { + "id": "43669ed5-bb65-432a-8b43-389a085fb4da", + "created_date": "2024-12-29 23:53:27.910528", + "last_modified_date": "2024-12-29 23:53:27.910528", + "version": 0, + "url": "https://ge.xhamster.com/videos/dbm-sex-society-xhRYvMF", + "review": 0, + "should_download": 0, + "title": "Dbm - Sexgesellschaft | xHamster", + "file_name": "Dbm - Sexgesellschaft [xhRYvMF].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/43669ed5-bb65-432a-8b43-389a085fb4da.mp4" + }, + { + "id": "43ea40a4-cd60-44a2-b5ef-de786c6c4a4e", + "created_date": "2024-10-21 15:08:43.547605", + "last_modified_date": "2024-10-21 16:27:52.649000", + "version": 1, + "url": "https://ge.xhamster.com/videos/gangbang-at-the-cocktail-bar-5737462", + "review": 0, + "should_download": 0, + "title": "Gangbang an der Cocktailbar | xHamster", + "file_name": "Gangbang an der Cocktailbar [5737462].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/43ea40a4-cd60-44a2-b5ef-de786c6c4a4e.mp4" + }, + { + "id": "44031dbc-8226-4a0e-add4-3e362f069e74", + "created_date": "2024-07-25 07:29:45.624898", + "last_modified_date": "2024-07-25 07:29:45.624898", + "version": 0, + "url": "https://ge.xhamster.com/videos/just-vintage-339-xhYtEPo", + "review": 0, + "should_download": 0, + "title": "Nur Retro 339 | xHamster", + "file_name": "Nur Retro 339 [xhYtEPo].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/44031dbc-8226-4a0e-add4-3e362f069e74.mp4" + }, + { + "id": "442b1d23-72d3-4b4a-9a91-0f05d538cc02", + "created_date": "2024-07-25 07:29:47.759426", + "last_modified_date": "2024-07-25 07:29:47.759426", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepmom-agrees-to-let-stepson-and-his-bff-dp-her-xhM7ESl", + "review": 0, + "should_download": 0, + "title": "Stiefmutter ist damit einverstanden, Stiefsohn und seine Freundin sie doppelpenetrieren zu lassen | xHamster", + "file_name": "Stiefmutter ist damit einverstanden, Stiefsohn und seine Freundin sie doppelpenetrieren zu lassen [xhM7ESl].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/442b1d23-72d3-4b4a-9a91-0f05d538cc02.mp4" + }, + { + "id": "44652828-e6dc-427f-a510-a873ebc602a5", + "created_date": "2024-07-25 07:29:45.367673", + "last_modified_date": "2024-07-25 07:29:45.367673", + "version": 0, + "url": "https://ge.xhamster.com/videos/naughty-schoolgirls-13473759", + "review": 0, + "should_download": 0, + "title": "Schulm\u00e4dchen im Reifetest | xHamster", + "file_name": "Schulm\u00e4dchen im Reifetest [13473759].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/44652828-e6dc-427f-a510-a873ebc602a5.mp4" + }, + { + "id": "44d408d0-803d-4c20-b2d0-2a649b8ce4f6", + "created_date": "2024-07-25 07:29:46.987038", + "last_modified_date": "2024-07-25 07:29:46.987038", + "version": 0, + "url": "https://ge.xhamster.com/videos/happy-wife-happy-life-s41-e15-xheo9jG", + "review": 0, + "should_download": 0, + "title": "Gl\u00fcckliche Ehefrau, gl\u00fcckliches Leben - s41: e15 | xHamster", + "file_name": "Gl\u00fcckliche Ehefrau, gl\u00fcckliches Leben - s41\uff1a e15 [xheo9jG].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/44d408d0-803d-4c20-b2d0-2a649b8ce4f6.mp4" + }, + { + "id": "44e900f4-d248-4959-a5fd-e47379309f8b", + "created_date": "2024-07-25 07:29:45.157943", + "last_modified_date": "2024-07-25 07:29:45.157943", + "version": 0, + "url": "https://ge.xhamster.com/videos/youre-not-listening-to-me-again-you-mean-girl-ill-make-you-study-xhqYaQK", + "review": 0, + "should_download": 0, + "title": "Du h\u00f6rst mir nicht wieder zu, du meinst m\u00e4dchen, ich werde dich zum lernen bringen! | xHamster", + "file_name": "Du h\u00f6rst mir nicht wieder zu, du meinst m\u00e4dchen, ich werde dich zum lernen bringen! [xhqYaQK].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/44e900f4-d248-4959-a5fd-e47379309f8b.mp4" + }, + { + "id": "4504a4bd-2e80-4500-a499-1536883a3f6c", + "created_date": "2024-07-25 07:29:45.804055", + "last_modified_date": "2024-07-25 07:29:45.804055", + "version": 0, + "url": "https://ge.xhamster.com/videos/robin-s-nest-1980-xhc3zbM", + "review": 0, + "should_download": 0, + "title": "Robin's Nest (1980) | xHamster", + "file_name": "Robin's Nest (1980) [xhc3zbM].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4504a4bd-2e80-4500-a499-1536883a3f6c.mp4" + }, + { + "id": "450c8826-6ac8-482f-9481-992af2fa8da5", + "created_date": "2024-07-25 07:29:47.874081", + "last_modified_date": "2024-07-25 07:29:47.874081", + "version": 0, + "url": "https://ge.xhamster.com/videos/getting-fucked-brings-her-joy-xhTYtEB", + "review": 0, + "should_download": 0, + "title": "Gefickt zu werden macht ihr Freude | xHamster", + "file_name": "Gefickt zu werden macht ihr Freude [xhTYtEB].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/450c8826-6ac8-482f-9481-992af2fa8da5.mp4" + }, + { + "id": "451b3813-4101-4611-9aa5-c01dc767c2f3", + "created_date": "2024-08-28 23:21:54.365379", + "last_modified_date": "2024-08-28 23:21:54.365379", + "version": 0, + "url": "https://ge.xhamster.com/videos/party-games-turn-into-party-sex-with-sakura-and-friends-xhkMtW1", + "review": 0, + "should_download": 0, + "title": "Partyspiele werden zu Partysex mit Sakura und Freunden | xHamster", + "file_name": "Partyspiele werden zu Partysex mit Sakura und Freunden [xhkMtW1].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/451b3813-4101-4611-9aa5-c01dc767c2f3.mp4" + }, + { + "id": "456953b6-a24d-4dc0-af79-bf710f88d7ac", + "created_date": "2024-07-25 07:29:46.068974", + "last_modified_date": "2024-07-25 07:29:46.068974", + "version": 0, + "url": "https://ge.xhamster.com/videos/group-sex-on-vacation-on-mallorca-xhgWLoV", + "review": 0, + "should_download": 0, + "title": "Geiler Gruppensex im Urlaub auf Mallorca | xHamster", + "file_name": "Geiler Gruppensex im Urlaub auf Mallorca [xhgWLoV].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/456953b6-a24d-4dc0-af79-bf710f88d7ac.mp4" + }, + { + "id": "45758a88-4bfb-4e42-a6e2-cb5b274b695e", + "created_date": "2024-07-25 07:29:45.166320", + "last_modified_date": "2024-07-25 07:29:45.166320", + "version": 0, + "url": "https://ge.xhamster.com/videos/college-teen-pussypounded-in-university-dorm-7219848", + "review": 0, + "should_download": 0, + "title": "College-Teen pussypounded im Studentenwohnheim | xHamster", + "file_name": "College-Teen pussypounded im Studentenwohnheim [7219848].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/45758a88-4bfb-4e42-a6e2-cb5b274b695e.mp4" + }, + { + "id": "460ab2c7-9b06-43fe-91e4-2b36ee9bc1dd", + "created_date": "2024-07-25 07:29:48.142999", + "last_modified_date": "2024-07-25 07:29:48.142999", + "version": 0, + "url": "https://ge.xhamster.com/videos/thirsty-work-1992-full-movie-xhWlLcH", + "review": 0, + "should_download": 0, + "title": "Durstige arbeit (1992) kompletter film | xHamster", + "file_name": "Durstige arbeit (1992) kompletter film [xhWlLcH].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/460ab2c7-9b06-43fe-91e4-2b36ee9bc1dd.mp4" + }, + { + "id": "4625b0f7-8f84-4ccb-b4b7-47ab8efefeeb", + "created_date": "2024-07-25 07:29:47.779586", + "last_modified_date": "2024-07-25 07:29:47.779586", + "version": 0, + "url": "https://ge.xhamster.com/videos/stossgebet-1979-10792310", + "review": 0, + "should_download": 0, + "title": "Stossgebet 1979: Free Retro Porn Video ac | xHamster", + "file_name": "Stossgebet (1979) [10792310].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4625b0f7-8f84-4ccb-b4b7-47ab8efefeeb.mp4" + }, + { + "id": "4679da1e-7a53-4a94-853f-ffd154aad647", + "created_date": "2024-07-25 07:29:44.318487", + "last_modified_date": "2024-07-25 07:29:44.318487", + "version": 0, + "url": "https://ge.xhamster.com/videos/taboo-fun-3806995", + "review": 0, + "should_download": 0, + "title": "Taboo Fun: Free Mature & MILF Porn Video 64 | xHamster", + "file_name": "taboo fun [3806995].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4679da1e-7a53-4a94-853f-ffd154aad647.mp4" + }, + { + "id": "46adba2b-5357-45db-a9a6-41d89f4fdc1a", + "created_date": "2024-07-25 07:29:47.418766", + "last_modified_date": "2024-07-25 07:29:47.418766", + "version": 0, + "url": "https://ge.xhamster.com/videos/german-family-secrets-episode-04-xh5vD3q", + "review": 0, + "should_download": 0, + "title": "Deutsche Familiengeheimnisse !!! - (Episode # 04) | xHamster", + "file_name": "Deutsche Familiengeheimnisse !!! - (Episode # 04) [xh5vD3q].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/46adba2b-5357-45db-a9a6-41d89f4fdc1a.mp4" + }, + { + "id": "46f2fbc6-d8b9-4aed-9024-428f066b509a", + "created_date": "2024-07-25 07:29:46.675345", + "last_modified_date": "2024-07-25 07:29:46.675345", + "version": 0, + "url": "https://ge.xhamster.com/videos/meet-the-nudists-7678194", + "review": 0, + "should_download": 0, + "title": "Treffen Sie die Nudisten | xHamster", + "file_name": "Treffen Sie die Nudisten [7678194].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/46f2fbc6-d8b9-4aed-9024-428f066b509a.mp4" + }, + { + "id": "46f79d78-587a-4b11-b536-47da2584126c", + "created_date": "2024-07-25 07:29:47.885135", + "last_modified_date": "2024-07-25 07:29:47.885135", + "version": 0, + "url": "https://ge.xhamster.com/videos/i-have-a-very-hands-on-approach-to-anatomy-xhpNmXX", + "review": 0, + "should_download": 0, + "title": "Ich habe einen sehr praktischen Zugang zur Anatomie | xHamster", + "file_name": "Ich habe einen sehr praktischen Zugang zur Anatomie [xhpNmXX].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/46f79d78-587a-4b11-b536-47da2584126c.mp4" + }, + { + "id": "471292ac-b5e2-4077-940d-3c18a14699c6", + "created_date": "2024-07-25 07:29:45.752821", + "last_modified_date": "2024-07-25 07:29:45.752821", + "version": 0, + "url": "https://ge.xhamster.com/videos/i-share-a-bed-with-my-stepmom-and-her-friend-then-we-fuck-xhURTsf", + "review": 0, + "should_download": 0, + "title": "ICH TEILE EIN BETT MIT MEINER STIEFMUTTER UND IHRER FREUNDIN, DANN FICKEN WIR. | xHamster", + "file_name": "ICH TEILE EIN BETT MIT MEINER STIEFMUTTER UND IHRER FREUNDIN, DANN FICKEN WIR. [xhURTsf].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/471292ac-b5e2-4077-940d-3c18a14699c6.mp4" + }, + { + "id": "474e2e7f-0654-448e-9144-c749371f96b5", + "created_date": "2024-07-25 07:29:47.088713", + "last_modified_date": "2024-07-25 07:29:47.088713", + "version": 0, + "url": "https://ge.xhamster.com/videos/bea-dumas-wedding-reception-orgy-this-is-hot-998036", + "review": 0, + "should_download": 0, + "title": "Bea Dumas Hochzeitsempfang-Orgie! Das ist hei\u00df !! | xHamster", + "file_name": "Bea Dumas Hochzeitsempfang-Orgie! Das ist hei\u00df !! [998036].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/474e2e7f-0654-448e-9144-c749371f96b5.mp4" + }, + { + "id": "47513996-9e2b-4a6c-ab1e-13dc3e6411eb", + "created_date": "2024-07-25 07:29:47.825054", + "last_modified_date": "2024-07-25 07:29:47.825054", + "version": 0, + "url": "https://ge.xhamster.com/videos/full-house-college-party-turns-into-hardcore-orgy-1583468", + "review": 0, + "should_download": 0, + "title": "Full House College-Party wird zu Hardcore-Orgie | xHamster", + "file_name": "Full House College-Party wird zu Hardcore-Orgie [1583468].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/47513996-9e2b-4a6c-ab1e-13dc3e6411eb.mp4" + }, + { + "id": "4752be84-66b9-4540-979e-d33aba799b08", + "created_date": "2024-07-25 07:29:47.585924", + "last_modified_date": "2024-07-25 07:29:47.585924", + "version": 0, + "url": "https://ge.xhamster.com/videos/kelly-trump-classic-hotel-of-pleasure-xhA7OFC", + "review": 0, + "should_download": 0, + "title": "Hotel der L\u00fcste | xHamster", + "file_name": "Hotel der L\u00fcste [xhA7OFC].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4752be84-66b9-4540-979e-d33aba799b08.mp4" + }, + { + "id": "477e96c7-07db-4103-a1df-39c2ba87b963", + "created_date": "2024-07-25 07:29:47.672621", + "last_modified_date": "2024-07-25 07:29:47.672621", + "version": 0, + "url": "https://ge.xhamster.com/videos/rural-holidays-1999-russian-full-video-hdtv-rip-xh61wb4", + "review": 0, + "should_download": 0, + "title": "L\u00e4ndliche Feiertage (1999, russisch, volles Video, hdtv rip) | xHamster", + "file_name": "L\u00e4ndliche Feiertage (1999, russisch, volles Video, hdtv rip) [xh61wb4].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/477e96c7-07db-4103-a1df-39c2ba87b963.mp4" + }, + { + "id": "47dc2c2b-590f-480e-9004-7ffed620845a", + "created_date": "2024-07-25 07:29:45.756413", + "last_modified_date": "2024-07-25 07:29:45.756413", + "version": 0, + "url": "https://ge.xhamster.com/videos/but-wait-you-were-only-supposed-to-jerk-him-off-lovita-fate-yells-at-ivi-rein-s3-e5-xhaBC2o", + "review": 0, + "should_download": 0, + "title": "\"Aber warten Sie! Sie sollten ihn nur wichsen!\" Lovita Fate schreit ivi rein -s3: e5 an | xHamster", + "file_name": "\uff02Aber warten Sie! Sie sollten ihn nur wichsen!\uff02 Lovita Fate schreit ivi rein -s3\uff1a e5 an [xhaBC2o].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/47dc2c2b-590f-480e-9004-7ffed620845a.mp4" + }, + { + "id": "47f81a5c-6c9b-4452-b9cd-6b64b2cdce8a", + "created_date": "2024-07-25 07:29:47.669210", + "last_modified_date": "2024-07-25 07:29:47.669210", + "version": 0, + "url": "https://ge.xhamster.com/videos/inside-desiree-cousteau-1979-xh47iIM", + "review": 0, + "should_download": 0, + "title": "Inside Desiree Cousteau (1979) | xHamster", + "file_name": "Inside Desiree Cousteau (1979) [xh47iIM].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/47f81a5c-6c9b-4452-b9cd-6b64b2cdce8a.mp4" + }, + { + "id": "48491c32-2de7-4e73-ab4c-d3ae50f67aa3", + "created_date": "2024-07-25 07:29:45.286540", + "last_modified_date": "2024-07-25 07:29:45.286540", + "version": 0, + "url": "https://ge.xhamster.com/videos/new-schoolgirl-lizz-gets-initiated-by-faye-and-her-roommates-14606088", + "review": 0, + "should_download": 0, + "title": "Neues Schulm\u00e4dchen Lizz wird von Faye und ihren Mitbewohnern initiiert | xHamster", + "file_name": "Neues Schulm\u00e4dchen Lizz wird von Faye und ihren Mitbewohnern initiiert [14606088].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/48491c32-2de7-4e73-ab4c-d3ae50f67aa3.mp4" + }, + { + "id": "48898536-8e2a-4234-9b12-22701dc622f3", + "created_date": "2024-07-25 07:29:47.403170", + "last_modified_date": "2024-07-25 07:29:47.403170", + "version": 0, + "url": "https://ge.xhamster.com/videos/classic-danish-gloryhole-xhvprSD", + "review": 0, + "should_download": 0, + "title": "Klassischer D\u00e4nisch - Gloryhole | xHamster", + "file_name": "Klassischer D\u00e4nisch - Gloryhole [xhvprSD].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/48898536-8e2a-4234-9b12-22701dc622f3.mp4" + }, + { + "id": "48932c6f-51f1-4c82-92ea-9127b78e6d86", + "created_date": "2024-09-11 10:23:29.175727", + "last_modified_date": "2024-10-21 16:27:59.603000", + "version": 1, + "url": "https://ge.xhamster.com/videos/step-mom-lets-step-son-free-use-his-step-sis-amber-moore-all-around-the-house-freeuse-fantasy-xhOvYEF", + "review": 0, + "should_download": 0, + "title": "Stiefmutter l\u00e4sst stiefsohn seine stiefschwester amber moore rund um das haus benutzen - freie fantasie | xHamster", + "file_name": "Stiefmutter l\u00e4sst stiefsohn seine stiefschwester amber moore rund um das haus benutzen - freie fantasie [xhOvYEF].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/48932c6f-51f1-4c82-92ea-9127b78e6d86.mp4" + }, + { + "id": "489be12e-90a6-4238-90e8-1120a77666aa", + "created_date": "2024-10-14 20:33:38.257194", + "last_modified_date": "2024-10-21 16:28:03.856000", + "version": 1, + "url": "https://ge.xhamster.com/videos/party-queen-3298277", + "review": 0, + "should_download": 0, + "title": "Partyk\u00f6nigin | xHamster", + "file_name": "Partyk\u00f6nigin [3298277].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/489be12e-90a6-4238-90e8-1120a77666aa.mp4" + }, + { + "id": "48c2c5de-6e77-4162-aeb2-65f905fda752", + "created_date": "2024-07-25 07:29:44.968836", + "last_modified_date": "2024-07-25 07:29:44.968836", + "version": 0, + "url": "https://ge.xhamster.com/videos/landschlampen-vol-2-full-movie-xh24aFo", + "review": 0, + "should_download": 0, + "title": "Landschlampen vol.2 (kompletter Film) | xHamster", + "file_name": "Landschlampen vol.2 (kompletter Film) [xh24aFo].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/48c2c5de-6e77-4162-aeb2-65f905fda752.mp4" + }, + { + "id": "48cadf46-73d1-444f-a93e-9ef0753d5611", + "created_date": "2024-07-25 07:29:46.034301", + "last_modified_date": "2024-07-25 07:29:46.034301", + "version": 0, + "url": "https://ge.xhamster.com/videos/now-this-is-how-to-party-723551", + "review": 0, + "should_download": 0, + "title": "Nun, so wird gefeiert | xHamster", + "file_name": "Nun, so wird gefeiert [723551].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/48cadf46-73d1-444f-a93e-9ef0753d5611.mp4" + }, + { + "id": "48d80238-6845-482c-a7ce-523c88558eff", + "created_date": "2024-07-25 07:29:44.281339", + "last_modified_date": "2024-07-25 07:29:44.281339", + "version": 0, + "url": "https://ge.xhamster.com/videos/what-movie-is-this-12278374", + "review": 0, + "should_download": 0, + "title": "Was f\u00fcr ein Film ist das? | xHamster", + "file_name": "Was f\u00fcr ein Film ist das\uff1f [12278374].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/48d80238-6845-482c-a7ce-523c88558eff.mp4" + }, + { + "id": "4932260e-32da-462d-8d33-591ecc24e3ff", + "created_date": "2024-07-25 07:29:45.404426", + "last_modified_date": "2024-07-25 07:29:45.404426", + "version": 0, + "url": "https://ge.xhamster.com/videos/missax-watching-porn-with-charlie-forde-xhSMBph", + "review": 0, + "should_download": 0, + "title": "MissaX - Porno gucken mit Charlie Forde | xHamster", + "file_name": "MissaX - Porno gucken mit Charlie Forde [xhSMBph].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/4932260e-32da-462d-8d33-591ecc24e3ff.mp4" + }, + { + "id": "49355c65-c1f9-49d3-aedd-4415629449c7", + "created_date": "2024-07-25 07:29:44.331429", + "last_modified_date": "2024-07-25 07:29:44.331429", + "version": 0, + "url": "https://ge.xhamster.com/videos/studentin-macht-gerne-die-beine-breit-xhaCkkP", + "review": 0, + "should_download": 0, + "title": "Studentin Macht Gerne Die Beine Breit, Porn a4 | xHamster", + "file_name": "Studentin macht gerne die Beine breit [xhaCkkP].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/49355c65-c1f9-49d3-aedd-4415629449c7.mp4" + }, + { + "id": "4944540e-3960-4905-9c6f-5af05e9fcd6f", + "created_date": "2024-08-08 01:01:27.749190", + "last_modified_date": "2024-08-08 01:01:27.749190", + "version": 0, + "url": null, + "review": 0, + "should_download": 0, + "title": null, + "file_name": "Familiensex [442392431].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4944540e-3960-4905-9c6f-5af05e9fcd6f.mp4" + }, + { + "id": "49812d30-3d9b-4f89-b80d-eec50c1e454e", + "created_date": "2024-07-25 07:29:45.662046", + "last_modified_date": "2024-07-25 07:29:45.662046", + "version": 0, + "url": "https://ge.xhamster.com/videos/step-son-and-his-best-friend-seduce-petite-step-mom-katrina-colt-to-fuck-in-a-threesome-mylf-taboo-xhIDUtM", + "review": 0, + "should_download": 0, + "title": "Stiefsohn und sein bester freund verf\u00fchren zierliche stiefmutter katrina colt zum dreier - MYLF tabu | xHamster", + "file_name": "Stiefsohn und sein bester freund verf\u00fchren zierliche stiefmutter katrina colt zum dreier - MYLF tabu [xhIDUtM].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/49812d30-3d9b-4f89-b80d-eec50c1e454e.mp4" + }, + { + "id": "498987ba-78e8-47a5-a5e0-27deaf5ab554", + "created_date": "2024-12-29 23:53:27.959278", + "last_modified_date": "2024-12-29 23:53:27.959278", + "version": 0, + "url": "https://ge.xhamster.com/videos/1976-jennifer-welles-of-a-young-american-house-wife-7516415", + "review": 0, + "should_download": 0, + "title": "1976 Jennifer Welles einer jungen amerikanischen Hausfrau | xHamster", + "file_name": "1976 Jennifer Welles einer jungen amerikanischen Hausfrau [7516415].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/498987ba-78e8-47a5-a5e0-27deaf5ab554.mp4" + }, + { + "id": "49fc1216-dd76-4c2d-8d9b-37a76d8ae976", + "created_date": "2024-07-25 07:29:45.293699", + "last_modified_date": "2024-07-25 07:29:45.293699", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-joys-of-our-neighborhood-3675552", + "review": 0, + "should_download": 0, + "title": "Die Freuden unserer Nachbarschaft | xHamster", + "file_name": "Die Freuden unserer Nachbarschaft [3675552].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/49fc1216-dd76-4c2d-8d9b-37a76d8ae976.mp4" + }, + { + "id": "4a3f670d-d8c6-47a8-a45d-3010a8f60872", + "created_date": "2024-07-25 07:29:46.510928", + "last_modified_date": "2024-07-25 07:29:46.510928", + "version": 0, + "url": "https://ge.xhamster.com/videos/one-of-the-dirtiest-families-ever-01-272364", + "review": 0, + "should_download": 0, + "title": "Eine der schmutzigsten Familien aller Zeiten | xHamster", + "file_name": "Eine der schmutzigsten Familien aller Zeiten [272364].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4a3f670d-d8c6-47a8-a45d-3010a8f60872.mp4" + }, + { + "id": "4a836b1e-b092-4efe-8d8f-03b250581b05", + "created_date": "2024-07-25 07:29:46.575999", + "last_modified_date": "2024-07-25 07:29:46.575999", + "version": 0, + "url": "https://ge.xhamster.com/videos/brother-step-sister-share-a-girlfriend-family-therapy-xhmMqlw", + "review": 0, + "should_download": 0, + "title": "Bruder und Stiefschwester teilen sich eine Freundin - Familientherapie | xHamster", + "file_name": "Bruder und Stiefschwester teilen sich eine Freundin - Familientherapie [xhmMqlw].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4a836b1e-b092-4efe-8d8f-03b250581b05.mp4" + }, + { + "id": "4aa684dc-07d4-4cae-893d-94683f47414d", + "created_date": "2025-01-19 13:42:34.178363", + "last_modified_date": "2025-01-19 13:42:34.178369", + "version": 0, + "url": "https://ge.xhamster.com/videos/passionate-mormon-teens-enjoy-pleasing-the-priests-xhyhWNM", + "review": 0, + "should_download": 0, + "title": "Leidenschaftliche mormonische teenager genie\u00dfen es, die priester zu befriedigen | xHamster", + "file_name": "Leidenschaftliche mormonische teenager genie\u00dfen es, die priester zu befriedigen [xhyhWNM].mp4", + "path": null, + "cloud_link": null + }, + { + "id": "4abd106c-1a12-44e8-97ef-c666b11dcff6", + "created_date": "2024-07-25 07:29:45.025535", + "last_modified_date": "2024-07-25 07:29:45.025535", + "version": 0, + "url": "https://ge.xhamster.com/videos/perfect-redhead-daughter-fucks-step-step-dad-opal-essex-xhQlllr", + "review": 0, + "should_download": 0, + "title": "Perfekte rothaarige Tochter fickt Stiefvater - Opal Essex | xHamster", + "file_name": "Perfekte rothaarige Tochter fickt Stiefvater - Opal Essex [xhQlllr].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4abd106c-1a12-44e8-97ef-c666b11dcff6.mp4" + }, + { + "id": "4b4544f4-f933-4945-82e7-1d8f3b1176e3", + "created_date": "2024-07-25 07:29:44.893563", + "last_modified_date": "2024-07-25 07:29:44.893563", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-best-group-fuck-in-town-1-xhYEPOj", + "review": 0, + "should_download": 0, + "title": "Der beste gruppenfick in der stadt 1 | xHamster", + "file_name": "Der beste gruppenfick in der stadt 1 [xhYEPOj].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4b4544f4-f933-4945-82e7-1d8f3b1176e3.mp4" + }, + { + "id": "4b5dfe03-1f9b-4fac-8576-0cc8d73841fb", + "created_date": "2024-07-25 07:29:44.934868", + "last_modified_date": "2024-07-25 07:29:44.934868", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-secrets-full-movie-xhvZIzI", + "review": 0, + "should_download": 0, + "title": "Familiengeheimnisse - kompletter Film | xHamster", + "file_name": "Familiengeheimnisse - kompletter Film [xhvZIzI].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/4b5dfe03-1f9b-4fac-8576-0cc8d73841fb.mp4" + }, + { + "id": "4b6f6480-f89b-4190-ad8c-81441e8708df", + "created_date": "2024-07-25 07:29:45.348018", + "last_modified_date": "2024-07-25 07:29:45.348018", + "version": 0, + "url": "https://ge.xhamster.com/videos/und-wieder-eine-total-versaute-familie-episode-04-xhY03LM", + "review": 0, + "should_download": 0, + "title": "Und Wieder Eine Total Versaute Familie - Episode 04 | xHamster", + "file_name": "Und wieder eine total versaute Familie - Episode #04 [xhY03LM].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4b6f6480-f89b-4190-ad8c-81441e8708df.mp4" + }, + { + "id": "4b8e3f08-3fdc-4857-809e-66add30c9ba8", + "created_date": "2024-07-25 07:29:47.152544", + "last_modified_date": "2024-07-25 07:29:47.152544", + "version": 0, + "url": "https://ge.xhamster.com/videos/nudist-sex-at-the-lake-7059674", + "review": 0, + "should_download": 0, + "title": "FKK-Sex am See | xHamster", + "file_name": "FKK-Sex am See [7059674].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4b8e3f08-3fdc-4857-809e-66add30c9ba8.mp4" + }, + { + "id": "4b95d8c6-f1ba-4371-be6c-2e88aefba1ff", + "created_date": "2024-08-28 23:21:54.373625", + "last_modified_date": "2024-08-28 23:21:54.373625", + "version": 0, + "url": "https://ge.xhamster.com/videos/naked-stranger-vhs-1987-xhG0GkR", + "review": 0, + "should_download": 0, + "title": "Nackter Fremder (vhs 1987) | xHamster", + "file_name": "Nackter Fremder (vhs 1987) [xhG0GkR].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/4b95d8c6-f1ba-4371-be6c-2e88aefba1ff.mp4" + }, + { + "id": "4ba4cf6a-76a1-459d-a9ca-36da83822c1e", + "created_date": "2024-07-25 07:29:47.220900", + "last_modified_date": "2024-07-25 07:29:47.220900", + "version": 0, + "url": "https://ge.xhamster.com/videos/lezioni-private-1975-5904006", + "review": 0, + "should_download": 0, + "title": "Lezioni Private 1975: Free MILF Porn Video 46 | xHamster", + "file_name": "Lezioni private (1975) [5904006].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4ba4cf6a-76a1-459d-a9ca-36da83822c1e.mp4" + }, + { + "id": "4bc31b4d-2926-4c4b-876a-f86f38078b52", + "created_date": "2024-07-25 07:29:44.582625", + "last_modified_date": "2024-07-25 07:29:44.582625", + "version": 0, + "url": "https://ge.xhamster.com/videos/rodox-boutique-voyeur-5572118", + "review": 0, + "should_download": 0, + "title": "Rodox Boutique Voyeur, Free Threesome Porn 4e | xHamster", + "file_name": "Rodox Boutique Voyeur [5572118].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4bc31b4d-2926-4c4b-876a-f86f38078b52.mp4" + }, + { + "id": "4be5562e-2dfe-468e-8763-3e9d945de656", + "created_date": "2024-07-25 07:29:46.023061", + "last_modified_date": "2024-07-25 07:29:46.023061", + "version": 0, + "url": "https://ge.xhamster.com/videos/outdoor-group-sex-party-6986638", + "review": 0, + "should_download": 0, + "title": "Outdoor-Gruppensex-Party | xHamster", + "file_name": "Outdoor-Gruppensex-Party [6986638].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4be5562e-2dfe-468e-8763-3e9d945de656.mp4" + }, + { + "id": "4bf4580b-6f16-434d-a359-4ca52ec5249c", + "created_date": "2024-07-25 07:29:46.457639", + "last_modified_date": "2024-07-25 07:29:46.457639", + "version": 0, + "url": "https://ge.xhamster.com/videos/after-soccer-training-chubby-gets-to-play-with-their-balls-4371418", + "review": 0, + "should_download": 0, + "title": "Nach dem Fu\u00dfballtraining darf die Mollige mit ihren Eiern spielen | xHamster", + "file_name": "Nach dem Fu\u00dfballtraining darf die Mollige mit ihren Eiern spielen [4371418].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4bf4580b-6f16-434d-a359-4ca52ec5249c.mp4" + }, + { + "id": "4bfaac0a-422d-42dc-86e9-fc4526c74c73", + "created_date": "2024-07-25 07:29:46.038466", + "last_modified_date": "2024-07-25 07:29:46.038466", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-strokes-helping-my-horny-stepbro-fuck-my-bff-xhjCo7D", + "review": 0, + "should_download": 0, + "title": "Family Strokes - meinem geilen Stiefbruder helfen, meine beste Freundin zu ficken | xHamster", + "file_name": "Family Strokes - meinem geilen Stiefbruder helfen, meine beste Freundin zu ficken [xhjCo7D].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4bfaac0a-422d-42dc-86e9-fc4526c74c73.mp4" + }, + { + "id": "4c3fbb06-e64a-4946-b914-2d2e5d55275c", + "created_date": "2025-01-16 19:59:34.587285", + "last_modified_date": "2025-01-16 19:59:34.587290", + "version": 0, + "url": "https://ge.xhamster.com/videos/welcome-to-my-horny-family-hot-mmff-orgy-xh5k8Wq", + "review": 0, + "should_download": 0, + "title": "Willkommen in meiner geilen familie - hei\u00dfe MMFF-orgie | xHamster", + "file_name": "Willkommen in meiner geilen familie - hei\u00dfe MMFF-orgie [xh5k8Wq].mp4", + "path": null, + "cloud_link": "/data/media/4c3fbb06-e64a-4946-b914-2d2e5d55275c.mp4" + }, + { + "id": "4c44c381-724f-4704-8ee1-698dca8837e1", + "created_date": "2024-09-05 20:03:45.743745", + "last_modified_date": "2024-10-21 16:28:10.462000", + "version": 1, + "url": "https://ge.xhamster.com/videos/group-sex-around-a-table-13234089", + "review": 0, + "should_download": 0, + "title": "Gruppensex rund um einen Tisch | xHamster", + "file_name": "Gruppensex rund um einen Tisch [13234089].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/4c44c381-724f-4704-8ee1-698dca8837e1.mp4" + }, + { + "id": "4c80e94d-f562-420d-b3ca-6c96b7957ef7", + "created_date": "2024-07-25 07:29:48.029059", + "last_modified_date": "2024-07-25 07:29:48.029059", + "version": 0, + "url": "https://ge.xhamster.com/videos/nubilefilms-horny-blonde-makes-big-brother-cum-6701760", + "review": 0, + "should_download": 0, + "title": "Nubilefilms geile Blondine l\u00e4sst gro\u00dfen Bruder kommen | xHamster", + "file_name": "Nubilefilms geile Blondine l\u00e4sst gro\u00dfen Bruder kommen [6701760].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4c80e94d-f562-420d-b3ca-6c96b7957ef7.mp4" + }, + { + "id": "4cf6541d-145c-4871-b484-68cc25320010", + "created_date": "2024-07-25 07:29:44.794548", + "last_modified_date": "2024-07-25 07:29:44.794548", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsis-says-you-can-cum-on-my-tits-now-but-later-i-want-you-to-cum-in-me-xhxblyn", + "review": 0, + "should_download": 0, + "title": "Stiefschwester sagt, du kannst jetzt auf meine Titten kommen, aber sp\u00e4ter will ich, dass du in mir kommst! | xHamster", + "file_name": "Stiefschwester sagt, du kannst jetzt auf meine Titten kommen, aber sp\u00e4ter will ich, dass du in mir kommst! [xhxblyn].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4cf6541d-145c-4871-b484-68cc25320010.mp4" + }, + { + "id": "4cf871b5-7ffc-4a1c-a6c9-3adecdf0c806", + "created_date": "2024-07-25 07:29:45.471280", + "last_modified_date": "2024-07-25 07:29:45.471280", + "version": 0, + "url": "https://ge.xhamster.com/videos/taboo-2-xh2JMI3", + "review": 0, + "should_download": 0, + "title": "Tabu 2 | xHamster", + "file_name": "Tabu 2 [xh2JMI3].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4cf871b5-7ffc-4a1c-a6c9-3adecdf0c806.mp4" + }, + { + "id": "4d007852-38b0-4504-9fab-1da578ec6ac5", + "created_date": "2024-07-25 07:29:44.714860", + "last_modified_date": "2024-07-25 07:29:44.714860", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-bosss-wife-seduced-me-to-fuck-her-and-cum-over-her-huge-tits-xhq6oVf", + "review": 0, + "should_download": 0, + "title": "Die frau meines chefs hat mich verf\u00fchrt, sie zu ficken und \u00fcber ihre riesigen titten zu kommen | xHamster", + "file_name": "Die frau meines chefs hat mich verf\u00fchrt, sie zu ficken und \u00fcber ihre riesigen titten zu kommen [xhq6oVf].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/4d007852-38b0-4504-9fab-1da578ec6ac5.mp4" + }, + { + "id": "4d2daf75-83ae-40a7-bb49-5ba7a95ab23e", + "created_date": "2024-07-25 07:29:44.348820", + "last_modified_date": "2024-07-25 07:29:44.348820", + "version": 0, + "url": "https://ge.xhamster.com/videos/hot-pool-party-xhF4ZAT", + "review": 0, + "should_download": 0, + "title": "Hei\u00dfe Pool-Party | xHamster", + "file_name": "Hei\u00dfe Pool-Party [xhF4ZAT].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4d2daf75-83ae-40a7-bb49-5ba7a95ab23e.mp4" + }, + { + "id": "4d45852d-870c-4e95-bd9f-5ee68e0b9d3c", + "created_date": "2024-09-24 08:11:39.003025", + "last_modified_date": "2024-10-21 16:28:14.856000", + "version": 1, + "url": "https://ge.xhamster.com/videos/stepmother-stepson-love-affair-pt-1-of-3-cory-chase-xh19Maa", + "review": 0, + "should_download": 0, + "title": "Stiefmutter und Stiefsohn lieben Aff\u00e4re Teil 1 von 3 - Cory Chase | xHamster", + "file_name": "Stiefmutter und Stiefsohn lieben Aff\u00e4re Teil 1 von 3 - Cory Chase [xh19Maa].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/4d45852d-870c-4e95-bd9f-5ee68e0b9d3c.mp4" + }, + { + "id": "4db45293-df98-407b-8a09-8a1005486495", + "created_date": "2024-07-25 07:29:48.162056", + "last_modified_date": "2024-07-25 07:29:48.162056", + "version": 0, + "url": "https://ge.xhamster.com/videos/house-of-strange-desires-1985-14901424", + "review": 0, + "should_download": 0, + "title": "Haus der seltsamen W\u00fcnsche (1985) | xHamster", + "file_name": "Haus der seltsamen W\u00fcnsche (1985) [14901424].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4db45293-df98-407b-8a09-8a1005486495.mp4" + }, + { + "id": "4dfdab0c-4e48-4945-b96c-91f4faa873ea", + "created_date": "2024-07-25 07:29:44.757053", + "last_modified_date": "2024-07-25 07:29:44.757053", + "version": 0, + "url": "https://ge.xhamster.com/videos/sex-roulette-dice-teen-orgy-with-michelle-honeywells-girls-7973553", + "review": 0, + "should_download": 0, + "title": "Sex-Roulette, W\u00fcrfel-Teen-Orgie mit Michelle Honeywells M\u00e4dchen | xHamster", + "file_name": "Sex-Roulette, W\u00fcrfel-Teen-Orgie mit Michelle Honeywells M\u00e4dchen [7973553].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4dfdab0c-4e48-4945-b96c-91f4faa873ea.mp4" + }, + { + "id": "4e4feedb-f42f-4083-89c4-db080cab54f8", + "created_date": "2024-07-25 07:29:45.959302", + "last_modified_date": "2024-07-25 07:29:45.959302", + "version": 0, + "url": "https://ge.xhamster.com/videos/jade-seduces-random-strangers-by-the-lake-and-fucks-one-of-them-xhKhVKp", + "review": 0, + "should_download": 0, + "title": "Jade verf\u00fchrt zuf\u00e4llige Fremde am See und fickt einen von ihnen | xHamster", + "file_name": "Jade verf\u00fchrt zuf\u00e4llige Fremde am See und fickt einen von ihnen [xhKhVKp].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4e4feedb-f42f-4083-89c4-db080cab54f8.mp4" + }, + { + "id": "4e65fc4d-9dd4-490e-bbab-b62ef661679f", + "created_date": "2024-07-25 07:29:47.074178", + "last_modified_date": "2024-07-25 07:29:47.074178", + "version": 0, + "url": "https://ge.xhamster.com/videos/familie-matuschek-with-anja-rochus-9564619", + "review": 0, + "should_download": 0, + "title": "Familie Matuschek with Anja Rochus, Free Porn 8c | xHamster", + "file_name": "Familie Matuschek with anja rochus [9564619].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4e65fc4d-9dd4-490e-bbab-b62ef661679f.mp4" + }, + { + "id": "4e860d0a-26b9-41fe-b8de-e85fac4fa043", + "created_date": "2024-07-25 07:29:48.122098", + "last_modified_date": "2024-07-25 07:29:48.122098", + "version": 0, + "url": "https://ge.xhamster.com/videos/erotic-family-affair-1984-14544731", + "review": 0, + "should_download": 0, + "title": "Erotic Family Affair (1984) | xHamster", + "file_name": "Erotic Family Affair (1984) [14544731].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4e860d0a-26b9-41fe-b8de-e85fac4fa043.mp4" + }, + { + "id": "4ea85153-d4c2-4f94-8d2f-fa58e26d76e3", + "created_date": "2024-07-25 07:29:47.251073", + "last_modified_date": "2024-07-25 07:29:47.251073", + "version": 0, + "url": "https://ge.xhamster.com/videos/college-teens-enjoy-cockriding-and-sucking-12333921", + "review": 0, + "should_download": 0, + "title": "College-Teenager genie\u00dfen Cockriding und Lutschen | xHamster", + "file_name": "College-Teenager genie\u00dfen Cockriding und Lutschen [12333921].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4ea85153-d4c2-4f94-8d2f-fa58e26d76e3.mp4" + }, + { + "id": "4ea98a2f-cc7c-4ef4-a84a-43ce5e35bb15", + "created_date": "2024-07-25 07:29:44.846490", + "last_modified_date": "2024-07-25 07:29:44.846490", + "version": 0, + "url": "https://ge.xhamster.com/videos/horny-hot-housewives-go-fishing-for-cum-1785321", + "review": 0, + "should_download": 0, + "title": "Geile hei\u00dfe Hausfrauen fischen auf Sperma | xHamster", + "file_name": "Geile hei\u00dfe Hausfrauen fischen auf Sperma [1785321].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4ea98a2f-cc7c-4ef4-a84a-43ce5e35bb15.mp4" + }, + { + "id": "4ebce3a1-a547-47dd-bfe7-c48767dbb0d8", + "created_date": "2024-07-25 07:29:45.185216", + "last_modified_date": "2024-07-25 07:29:45.185216", + "version": 0, + "url": "https://ge.xhamster.com/videos/mature-mom-gets-even-with-son-by-fucking-his-best-friend-xhFquSd", + "review": 0, + "should_download": 0, + "title": "REIFE MUTTER bekommt sogar mit sohn, indem sie seinen besten freund fickt! | xHamster", + "file_name": "REIFE MUTTER bekommt sogar mit sohn, indem sie seinen besten freund fickt! [xhFquSd].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/4ebce3a1-a547-47dd-bfe7-c48767dbb0d8.mp4" + }, + { + "id": "4f17bde6-7e09-4a34-b541-9e12b386349f", + "created_date": "2024-07-25 07:29:48.179932", + "last_modified_date": "2024-07-25 07:29:48.179932", + "version": 0, + "url": "https://ge.xhamster.com/videos/group-sex-of-students-at-lake-1947714", + "review": 0, + "should_download": 0, + "title": "Gruppensex von Studenten am See | xHamster", + "file_name": "Gruppensex von Studenten am See [1947714].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4f17bde6-7e09-4a34-b541-9e12b386349f.mp4" + }, + { + "id": "4f1a7a2c-e446-420f-9278-a88081c94a6b", + "created_date": "2024-07-25 07:29:47.745312", + "last_modified_date": "2024-07-25 07:29:47.745312", + "version": 0, + "url": "https://ge.xhamster.com/videos/chubby-french-milf-mylene-agrees-to-a-threesome-but-not-a-foursome-xhEwin7", + "review": 0, + "should_download": 0, + "title": "Dreier war OK f\u00fcr Sie, Vierer war dann doch zu viel | xHamster", + "file_name": "Dreier war OK f\u00fcr Sie, Vierer war dann doch zu viel [xhEwin7].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4f1a7a2c-e446-420f-9278-a88081c94a6b.mp4" + }, + { + "id": "4f3553c8-43f8-4e63-b35f-12ef5e67c609", + "created_date": "2024-07-25 07:29:46.230470", + "last_modified_date": "2024-07-25 07:29:46.230470", + "version": 0, + "url": "https://ge.xhamster.com/videos/im-obsessed-with-my-ginger-stepdaughters-perfect-breasts-xhvBAB6", + "review": 0, + "should_download": 0, + "title": "Ich bin besessen von den perfekten Br\u00fcsten meiner rothaarigen Stieftochter | xHamster", + "file_name": "Ich bin besessen von den perfekten Br\u00fcsten meiner rothaarigen Stieftochter [xhvBAB6].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4f3553c8-43f8-4e63-b35f-12ef5e67c609.mp4" + }, + { + "id": "4f5dfb73-7dc6-4980-ba39-3323f40d4f43", + "created_date": "2024-07-25 07:29:48.184831", + "last_modified_date": "2024-07-25 07:29:48.184831", + "version": 0, + "url": "https://ge.xhamster.com/videos/araceli-looks-for-a-guy-to-fuck-around-a-nude-beach-xh2cW8k", + "review": 0, + "should_download": 0, + "title": "Araceli sucht nach einem Typen, der an einem FKK-Strand fickt | xHamster", + "file_name": "Araceli sucht nach einem Typen, der an einem FKK-Strand fickt [xh2cW8k].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4f5dfb73-7dc6-4980-ba39-3323f40d4f43.mp4" + }, + { + "id": "4fa0e220-ca8c-44d8-a709-a1144880d061", + "created_date": "2024-10-21 15:08:43.545288", + "last_modified_date": "2024-10-21 16:28:19.587000", + "version": 1, + "url": "https://ge.xhamster.com/videos/first-sandwich-xh1ci0h", + "review": 0, + "should_download": 0, + "title": "Erstes Sandwich | xHamster", + "file_name": "Erstes Sandwich [xh1ci0h].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/4fa0e220-ca8c-44d8-a709-a1144880d061.mp4" + }, + { + "id": "4fc46abd-abfb-49d2-9b07-e2d234a25a38", + "created_date": "2024-07-25 07:29:48.158533", + "last_modified_date": "2024-07-25 07:29:48.158533", + "version": 0, + "url": "https://ge.xhamster.com/videos/hot-teacher-double-anal-stormy-gale-12710987", + "review": 0, + "should_download": 0, + "title": "Hei\u00dfe Lehrerin, doppelter analer st\u00fcrmischer Sturm | xHamster", + "file_name": "Hei\u00dfe Lehrerin, doppelter analer st\u00fcrmischer Sturm [12710987].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4fc46abd-abfb-49d2-9b07-e2d234a25a38.mp4" + }, + { + "id": "4fcabce2-8cb3-4f35-8431-849252f8a29b", + "created_date": "2024-07-25 07:29:47.790293", + "last_modified_date": "2024-07-25 07:29:47.790293", + "version": 0, + "url": "https://ge.xhamster.com/videos/reality-kings-euro-sex-parties-sharing-and-caring-tina-9798051", + "review": 0, + "should_download": 0, + "title": "Reality - K\u00f6nige - Euro - Sexpartys - Teilen und F\u00fcrsorge - Tina | xHamster", + "file_name": "Reality - K\u00f6nige - Euro - Sexpartys - Teilen und F\u00fcrsorge - Tina [9798051].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4fcabce2-8cb3-4f35-8431-849252f8a29b.mp4" + }, + { + "id": "4ff668c7-52ab-4a21-bdef-bc461e27b34f", + "created_date": "2024-07-25 07:29:46.702222", + "last_modified_date": "2024-07-25 07:29:46.702222", + "version": 0, + "url": "https://ge.xhamster.com/videos/schweine-priester-2-die-beichte-11653992", + "review": 0, + "should_download": 0, + "title": "Schweine Priester 2 - Die Beichte, Free Porn 58 | xHamster", + "file_name": "Schweine Priester 2 - Die Beichte [11653992].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/4ff668c7-52ab-4a21-bdef-bc461e27b34f.mp4" + }, + { + "id": "4ffedc25-db73-4158-ae9a-f6b60521032c", + "created_date": "2024-10-07 20:47:56.423927", + "last_modified_date": "2024-10-21 16:28:26.753000", + "version": 1, + "url": "https://ge.xhamster.com/videos/ich-bin-jung-und-brauche-das-geld-nr-8-episode-3-xhmAfyy", + "review": 0, + "should_download": 0, + "title": "Ich Bin Jung - Und Brauche Das Geld Nr 8 - Episode 3 | xHamster", + "file_name": "Ich bin jung - und brauche das geld Nr.8 - Episode 3 [xhmAfyy].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/4ffedc25-db73-4158-ae9a-f6b60521032c.mp4" + }, + { + "id": "50029ef3-e276-4022-a685-981f54c6493e", + "created_date": "2024-07-25 07:29:46.706778", + "last_modified_date": "2024-07-25 07:29:46.706778", + "version": 0, + "url": "https://ge.xhamster.com/videos/kuken-3-episode-6-xhdmel3", + "review": 0, + "should_download": 0, + "title": "Kuken 3 - Episode 6: Free Big Cock Porn Video bf | xHamster", + "file_name": "KUKEN 3 - Episode 6 [xhdmel3].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/50029ef3-e276-4022-a685-981f54c6493e.mp4" + }, + { + "id": "5019cbfb-aa96-4efc-a929-969deb0ecda5", + "created_date": "2024-07-25 07:29:45.001056", + "last_modified_date": "2024-07-25 07:29:45.001056", + "version": 0, + "url": "https://ge.xhamster.com/videos/amazing-horny-wife-rent-pussy-fucked-by-husbands-friend-xhd70dc", + "review": 0, + "should_download": 0, + "title": "Erstaunliche geile Ehefrau Miete Muschi von Freund ihres Mannes gefickt | xHamster", + "file_name": "Erstaunliche geile Ehefrau Miete Muschi von Freund ihres Mannes gefickt [xhd70dc].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5019cbfb-aa96-4efc-a929-969deb0ecda5.mp4" + }, + { + "id": "50b175e7-8aee-4b74-bbde-0f8ab5fa4548", + "created_date": "2024-07-25 07:29:47.280052", + "last_modified_date": "2024-07-25 07:29:47.280052", + "version": 0, + "url": "https://ge.xhamster.com/videos/if-the-2-of-you-need-to-fuck-something-it-better-be-me-swapmom-cory-chase-demands-swap-family-s7-e2-xhVjuur", + "review": 0, + "should_download": 0, + "title": "\"Wenn die 2 von dir etwas ficken muss, bin ich besser ich!\" Swapmom Cory Chase verlangt den tausch der familie - S7: E2 | xHamster", + "file_name": "\uff02Wenn die 2 von dir etwas ficken muss, bin ich besser ich!\uff02 Swapmom Cory Chase verlangt den tausch der familie - S7\uff1a E2 [xhVjuur].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/50b175e7-8aee-4b74-bbde-0f8ab5fa4548.mp4" + }, + { + "id": "50b99054-5c7f-445b-8a34-d89329723e35", + "created_date": "2024-07-25 07:29:48.132684", + "last_modified_date": "2024-07-25 07:29:48.132684", + "version": 0, + "url": "https://ge.xhamster.com/videos/slutty-stepsister-scarlet-skies-moans-omg-i-want-every-fucking-inch-of-you-xhZfXLq", + "review": 0, + "should_download": 0, + "title": "Die versaute Stiefschwester Scarlet Skies st\u00f6hnt, \"omg, ich will jeden verdammten Zoll von dir\" | xHamster", + "file_name": "Die versaute Stiefschwester Scarlet Skies st\u00f6hnt, \uff02omg, ich will jeden verdammten Zoll von dir\uff02 [xhZfXLq].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/50b99054-5c7f-445b-8a34-d89329723e35.mp4" + }, + { + "id": "50c9828a-c9a0-4155-b331-54cf834c35fd", + "created_date": "2025-01-19 13:42:35.568910", + "last_modified_date": "2025-01-19 13:42:35.568916", + "version": 0, + "url": "https://ge.xhamster.com/videos/busty-step-moms-catch-their-respective-stepsons-masturbating-help-them-release-the-sexual-tension-xhxyTon", + "review": 0, + "should_download": 0, + "title": "Vollbusige stiefmutter erwischen ihre jeweiligen stiefsohn beim masturbieren und helfen ihnen, die sexuelle spannung zu l\u00f6sen | xHamster", + "file_name": "Vollbusige stiefmutter erwischen ihre jeweiligen stiefsohn beim masturbieren und helfen ihnen, die sexuelle spannung zu l\u00f6sen [xhxyTon].mp4", + "path": null, + "cloud_link": null + }, + { + "id": "5121c078-2917-4115-b879-2b57efdfdef6", + "created_date": "2024-07-25 07:29:48.155011", + "last_modified_date": "2024-07-25 07:29:48.155011", + "version": 0, + "url": "https://ge.xhamster.com/videos/deutschland-secrets-part-04-xh34sWp", + "review": 0, + "should_download": 0, + "title": "Deutschland Secrets - Part 04, Free European Porn Video 26 | xHamster", + "file_name": "Deutschland Secrets!!! - part #04 [xh34sWp].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5121c078-2917-4115-b879-2b57efdfdef6.mp4" + }, + { + "id": "5123d7ee-8d59-432f-bb15-582661e2b79a", + "created_date": "2024-07-25 07:29:45.888565", + "last_modified_date": "2024-07-25 07:29:45.888565", + "version": 0, + "url": "https://ge.xhamster.com/videos/sex-in-the-office-for-mya-diamond-xhagKYd", + "review": 0, + "should_download": 0, + "title": "Sex im B\u00fcro f\u00fcr Mya Diamond | xHamster", + "file_name": "Sex im B\u00fcro f\u00fcr Mya Diamond [xhagKYd].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5123d7ee-8d59-432f-bb15-582661e2b79a.mp4" + }, + { + "id": "517e6d4a-c0ab-4f6f-aeb9-459f0d80d885", + "created_date": "2024-07-25 07:29:46.607308", + "last_modified_date": "2024-07-25 07:29:46.607308", + "version": 0, + "url": "https://ge.xhamster.com/videos/nurumassage-chanel-preston-loves-giving-spicy-happy-endings-xh4rG7Q", + "review": 0, + "should_download": 0, + "title": "In Nurumassage liebt Chanel Preston w\u00fcrzige Happy Ends | xHamster", + "file_name": "In Nurumassage liebt Chanel Preston w\u00fcrzige Happy Ends [xh4rG7Q].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/517e6d4a-c0ab-4f6f-aeb9-459f0d80d885.mp4" + }, + { + "id": "5207b7eb-e076-4789-9257-002b888a4d0e", + "created_date": "2024-07-25 07:29:46.841301", + "last_modified_date": "2024-07-25 07:29:46.841301", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepdad-fucks-me-xhkddTh", + "review": 0, + "should_download": 0, + "title": "Stiefvater fickt mich | xHamster", + "file_name": "Stiefvater fickt mich [xhkddTh].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5207b7eb-e076-4789-9257-002b888a4d0e.mp4" + }, + { + "id": "521d8d21-31c7-4f56-9809-497d2895b665", + "created_date": "2024-07-25 07:29:46.764390", + "last_modified_date": "2024-07-25 07:29:46.764390", + "version": 0, + "url": "https://ge.xhamster.com/videos/sex-party-with-eva-kleber-9786674", + "review": 0, + "should_download": 0, + "title": "Sexparty mit Eva Kleber | xHamster", + "file_name": "Sexparty mit Eva Kleber [9786674].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/521d8d21-31c7-4f56-9809-497d2895b665.mp4" + }, + { + "id": "52b99664-d337-43ec-bfa5-ebcd3cdb731d", + "created_date": "2024-07-25 07:29:45.419222", + "last_modified_date": "2024-07-25 07:29:45.419222", + "version": 0, + "url": "https://ge.xhamster.com/videos/real-slut-party-jynx-maze-three-to-one-advantage-mofos-10366980", + "review": 0, + "should_download": 0, + "title": "Echte Schlampenparty - Jynx Labyrinth - drei zu einem Vorteil - Mofos | xHamster", + "file_name": "Echte Schlampenparty - Jynx Labyrinth - drei zu einem Vorteil - Mofos [10366980].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/52b99664-d337-43ec-bfa5-ebcd3cdb731d.mp4" + }, + { + "id": "52c70ddc-480b-472d-9c21-d4a629433db7", + "created_date": "2024-07-25 07:29:46.273594", + "last_modified_date": "2024-07-25 07:29:46.273594", + "version": 0, + "url": "https://ge.xhamster.com/videos/smart-guy-proposed-pretty-babe-to-hang-around-him-in-the-pool-one-hot-summer-day-xhzLX8b", + "review": 0, + "should_download": 0, + "title": "Kluger Typ schlug ein h\u00fcbsches Sch\u00e4tzchen vor, sich an einem hei\u00dfen Sommertag um ihn im Pool zu h\u00e4ngen | xHamster", + "file_name": "Kluger Typ schlug ein h\u00fcbsches Sch\u00e4tzchen vor, sich an einem hei\u00dfen Sommertag um ihn im Pool zu h\u00e4ngen [xhzLX8b].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/52c70ddc-480b-472d-9c21-d4a629433db7.mp4" + }, + { + "id": "52de336f-c448-4e44-9a8f-e4dc46806f0e", + "created_date": "2024-07-25 07:29:46.453662", + "last_modified_date": "2024-07-25 07:29:46.453662", + "version": 0, + "url": "https://ge.xhamster.com/videos/me-and-my-stepdad-fuck-while-mom-is-away-full-movie-xhgdB4e", + "review": 0, + "should_download": 0, + "title": "Ich und mein Stiefvater ficken, w\u00e4hrend Mutter weg ist - kompletter Film | xHamster", + "file_name": "Ich und mein Stiefvater ficken, w\u00e4hrend Mutter weg ist - kompletter Film [xhgdB4e].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/52de336f-c448-4e44-9a8f-e4dc46806f0e.mp4" + }, + { + "id": "534cf3a7-f96d-4b70-9c50-8c02cf01640b", + "created_date": "2024-07-25 07:29:45.602307", + "last_modified_date": "2024-07-25 07:29:45.602307", + "version": 0, + "url": "https://ge.xhamster.com/videos/sex-office-part-4-fucked-a-feminist-like-a-whore-xhf2Rne", + "review": 0, + "should_download": 0, + "title": "Sexb\u00fcro, teil 4. Eine feministin wie eine hure gefickt! | xHamster", + "file_name": "Sexb\u00fcro, teil 4. Eine feministin wie eine hure gefickt! [xhf2Rne].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/534cf3a7-f96d-4b70-9c50-8c02cf01640b.mp4" + }, + { + "id": "53709293-c7ea-4859-b27d-a1fac53af07f", + "created_date": "2024-07-25 07:29:47.714984", + "last_modified_date": "2024-07-25 07:29:47.714984", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepbro-assfucked-beauty-and-cum-in-mouth-after-deepthroat-xhFMldy", + "review": 0, + "should_download": 0, + "title": "Stiefbruder Arschfick, Sch\u00f6nheit und Sperma im Mund nach Halsfick | xHamster", + "file_name": "Stiefbruder Arschfick, Sch\u00f6nheit und Sperma im Mund nach Halsfick [xhFMldy].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/53709293-c7ea-4859-b27d-a1fac53af07f.mp4" + }, + { + "id": "53d746e5-03ff-4c86-b52b-7382e7781ab9", + "created_date": "2024-11-10 16:53:33.493311", + "last_modified_date": "2024-11-10 16:53:33.493311", + "version": 0, + "url": "https://ge.xhamster.com/videos/girls-in-heat-1979-xhhI01M", + "review": 0, + "should_download": 0, + "title": "Girls in Heat 1979 | xHamster", + "file_name": "Girls in Heat 1979 [xhhI01M].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/53d746e5-03ff-4c86-b52b-7382e7781ab9.mp4" + }, + { + "id": "53f5423d-59d0-46d1-a37c-d80b1d77d248", + "created_date": "2024-07-25 07:29:45.666460", + "last_modified_date": "2024-07-25 07:29:45.666460", + "version": 0, + "url": "https://ge.xhamster.com/videos/truth-or-dare-3-6186855", + "review": 0, + "should_download": 0, + "title": "Truth or Dare 3: Free Amateur HD Porn Video 89 | xHamster", + "file_name": "Truth or Dare 3 [6186855].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/53f5423d-59d0-46d1-a37c-d80b1d77d248.mp4" + }, + { + "id": "54227ce7-9428-4e2d-b4ae-77bddf17ce52", + "created_date": "2024-07-25 07:29:44.886648", + "last_modified_date": "2024-07-25 07:29:44.886648", + "version": 0, + "url": "https://ge.xhamster.com/videos/paare-ueberkommt-die-lust-im-pornokino-in-berlin-1-6581727", + "review": 0, + "should_download": 0, + "title": "Paare Ueberkommt Die Lust Im Pornokino in Berlin 1: Porn 3a | xHamster", + "file_name": "Paare ueberkommt die Lust im Pornokino in Berlin 1 [6581727].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/54227ce7-9428-4e2d-b4ae-77bddf17ce52.mp4" + }, + { + "id": "54326e45-7267-4045-b77b-f8e9cb51a274", + "created_date": "2024-10-07 20:47:56.413263", + "last_modified_date": "2024-10-21 16:28:34.984000", + "version": 1, + "url": "https://ge.xhamster.com/videos/naughty-teen-girlfriend-home-anal-threesome-with-facials-9106304", + "review": 0, + "should_download": 0, + "title": "Freche Teen-Freundin zu Hause anal Dreier mit Gesichtsbesamungen | xHamster", + "file_name": "Freche Teen-Freundin zu Hause anal Dreier mit Gesichtsbesamungen [9106304].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/54326e45-7267-4045-b77b-f8e9cb51a274.mp4" + }, + { + "id": "545b0be8-167b-4d09-b2b9-7430213611b7", + "created_date": "2024-08-28 23:21:54.356286", + "last_modified_date": "2024-08-28 23:21:54.356286", + "version": 0, + "url": "https://ge.xhamster.com/videos/fucking-during-a-pool-party-is-so-much-fun-for-these-beautiful-teens-xhMzvsJ", + "review": 0, + "should_download": 0, + "title": "Ficken w\u00e4hrend einer Pool-Party macht so viel Spa\u00df f\u00fcr diese sch\u00f6nen Teenager | xHamster", + "file_name": "Ficken w\u00e4hrend einer Pool-Party macht so viel Spa\u00df f\u00fcr diese sch\u00f6nen Teenager [xhMzvsJ].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/545b0be8-167b-4d09-b2b9-7430213611b7.mp4" + }, + { + "id": "5472a49b-7bc5-4103-a5ce-6326911455ca", + "created_date": "2024-07-25 07:29:44.258863", + "last_modified_date": "2024-07-25 07:29:44.258863", + "version": 0, + "url": "https://ge.xhamster.com/videos/dads-dirty-movies-8-1981-2144857", + "review": 0, + "should_download": 0, + "title": "Papas schmutzige Filme 8 - 1981 | xHamster", + "file_name": "Papas schmutzige Filme 8 - 1981 [2144857].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5472a49b-7bc5-4103-a5ce-6326911455ca.mp4" + }, + { + "id": "54f2f917-95a9-4c44-ab51-5d31d722dcea", + "created_date": "2024-07-25 07:29:45.842919", + "last_modified_date": "2024-07-25 07:29:45.842919", + "version": 0, + "url": "https://ge.xhamster.com/videos/two-couple-have-group-sex-at-beach-xh0AQpR", + "review": 0, + "should_download": 0, + "title": "Zwei paare haben gruppensex am strand | xHamster", + "file_name": "Zwei paare haben gruppensex am strand [xh0AQpR].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/54f2f917-95a9-4c44-ab51-5d31d722dcea.mp4" + }, + { + "id": "551169a2-9e1e-4136-856c-b1588e9b1cc4", + "created_date": "2024-07-25 07:29:47.842916", + "last_modified_date": "2024-07-25 07:29:47.842916", + "version": 0, + "url": "https://ge.xhamster.com/videos/flying-acquaintances-1973-us-jamie-gillis-softcore-dvd-xhlILiu", + "review": 0, + "should_download": 0, + "title": "Fliegende Bekannte (1973, wir, Jamie Gillis, Softcore, DVD) | xHamster", + "file_name": "Fliegende Bekannte (1973, wir, Jamie Gillis, Softcore, DVD) [xhlILiu].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/551169a2-9e1e-4136-856c-b1588e9b1cc4.mp4" + }, + { + "id": "5563cb67-686c-4909-aeca-eb12ec0495d8", + "created_date": "2024-07-25 07:29:48.189497", + "last_modified_date": "2024-07-25 07:29:48.189497", + "version": 0, + "url": "https://ge.xhamster.com/videos/daddy4k-his-post-traumatic-stress-disorder-caused-the-wildest-sex-of-her-life-xhQTJKA", + "review": 0, + "should_download": 0, + "title": "DADDY4K. Seine posttraumatische Stressst\u00f6rung verursachte den wildesten sex ihres Lebens | xHamster", + "file_name": "DADDY4K. Seine posttraumatische Stressst\u00f6rung verursachte den wildesten sex ihres Lebens [xhQTJKA].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5563cb67-686c-4909-aeca-eb12ec0495d8.mp4" + }, + { + "id": "55649871-846b-4e76-950c-6b0f3e815b15", + "created_date": "2024-07-25 07:29:47.683167", + "last_modified_date": "2024-07-25 07:29:47.683167", + "version": 0, + "url": "https://ge.xhamster.com/videos/emily-willis-says-to-sky-oh-my-god-come-here-you-have-to-taste-your-stepbros-cock-s11-e12-xhxSBsf", + "review": 0, + "should_download": 0, + "title": "Emily Willis sagt zu Sky: \"Oh mein Gott, komm her, du musst den Schwanz deines Stiefbruders probieren\" - s11: e12 | xHamster", + "file_name": "Emily Willis sagt zu Sky\uff1a \uff02Oh mein Gott, komm her, du musst den Schwanz deines Stiefbruders probieren\uff02 - s11\uff1a e12 [xhxSBsf].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/55649871-846b-4e76-950c-6b0f3e815b15.mp4" + }, + { + "id": "5579981c-0b0e-4af8-a80d-f8f5a50081c8", + "created_date": "2024-07-25 07:29:46.088138", + "last_modified_date": "2024-07-25 07:29:46.088138", + "version": 0, + "url": "https://ge.xhamster.com/videos/get-out-of-my-room-you-perv-6036597", + "review": 0, + "should_download": 0, + "title": "Raus aus meinem Zimmer, du Perverser! | xHamster", + "file_name": "Raus aus meinem Zimmer, du Perverser! [6036597].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5579981c-0b0e-4af8-a80d-f8f5a50081c8.mp4" + }, + { + "id": "56453e6f-fd97-4f0e-bc58-4a23b7d23076", + "created_date": "2024-11-10 16:53:33.488529", + "last_modified_date": "2024-11-10 16:53:33.488529", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-swingers-10408941", + "review": 0, + "should_download": 0, + "title": "Familien-Swinger | xHamster", + "file_name": "Familien-Swinger [10408941].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/56453e6f-fd97-4f0e-bc58-4a23b7d23076.mp4" + }, + { + "id": "56598d06-e04b-4904-aa7e-8cec9dfdf0d6", + "created_date": "2024-07-25 07:29:47.881238", + "last_modified_date": "2024-07-25 07:29:47.881238", + "version": 0, + "url": "https://ge.xhamster.com/videos/pool-side-blowjob-orgy-1570838", + "review": 0, + "should_download": 0, + "title": "Pool-Seite Blowjob-Orgie | xHamster", + "file_name": "Pool-Seite Blowjob-Orgie [1570838].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/56598d06-e04b-4904-aa7e-8cec9dfdf0d6.mp4" + }, + { + "id": "56c6ec9c-880b-41bb-b82b-251b5c17682f", + "created_date": "2024-07-25 07:29:47.596870", + "last_modified_date": "2024-07-25 07:29:47.596870", + "version": 0, + "url": "https://ge.xhamster.com/videos/blonde-fucked-hard-in-a-boat-on-the-lake-three-guys-1918591", + "review": 0, + "should_download": 0, + "title": "Blondine hart gefickt in einem Boot auf dem See drei Typen | xHamster", + "file_name": "Blondine hart gefickt in einem Boot auf dem See drei Typen [1918591].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/56c6ec9c-880b-41bb-b82b-251b5c17682f.mp4" + }, + { + "id": "572a6e1b-6b59-4d15-b951-37b9eca47686", + "created_date": "2024-07-25 07:29:44.242011", + "last_modified_date": "2024-07-25 07:29:44.242011", + "version": 0, + "url": "https://ge.xhamster.com/videos/if-i-get-naked-you-have-to-get-naked-too-scarlett-hampton-tells-swapbro-s6-e3-xhnseD1", + "review": 0, + "should_download": 0, + "title": "\"Wenn ich mich nackig mache, musst du dich auch nackt machen\", sagt Scarlett Hampton zu Swapbro -s6: e3 | xHamster", + "file_name": "\uff02Wenn ich mich nackig mache, musst du dich auch nackt machen\uff02, sagt Scarlett Hampton zu Swapbro -s6\uff1a e3 [xhnseD1].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/572a6e1b-6b59-4d15-b951-37b9eca47686.mp4" + }, + { + "id": "5737b039-4ebd-4a95-b9b7-5d5cb6b55bf9", + "created_date": "2024-07-25 07:29:47.182974", + "last_modified_date": "2024-07-25 07:29:47.182974", + "version": 0, + "url": "https://ge.xhamster.com/videos/bumsfidele-hochzeitsnacht-5805485", + "review": 0, + "should_download": 0, + "title": "Bumsfidele Hochzeitsnacht, Free Vintage Porn 10 | xHamster", + "file_name": "Bumsfidele Hochzeitsnacht [5805485].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5737b039-4ebd-4a95-b9b7-5d5cb6b55bf9.mp4" + }, + { + "id": "57ad263d-b58f-40b6-a4ea-7bc6fb18e54d", + "created_date": "2024-07-25 07:29:44.779371", + "last_modified_date": "2024-07-25 07:29:44.779371", + "version": 0, + "url": "https://ge.xhamster.com/videos/young-son-shares-first-girlfriend-with-his-stepfather-xhKFmcc", + "review": 0, + "should_download": 0, + "title": "Junger Sohn teilt erste Freundin mit seinem Stiefvater | xHamster", + "file_name": "Junger Sohn teilt erste Freundin mit seinem Stiefvater [xhKFmcc].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/57ad263d-b58f-40b6-a4ea-7bc6fb18e54d.mp4" + }, + { + "id": "57bf1fdc-d16f-46f9-9342-5e0e6c6366aa", + "created_date": "2024-07-25 07:29:46.216935", + "last_modified_date": "2024-07-25 07:29:46.216935", + "version": 0, + "url": "https://ge.xhamster.desi/videos/stepsis-says-do-you-have-a-boner-xhCYUy5", + "review": 0, + "should_download": 0, + "title": "Stiefschwester sagt, hast du eine Latte ?! | xHamster", + "file_name": "Stiefschwester sagt, hast du eine Latte \uff1f! [xhCYUy5].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/57bf1fdc-d16f-46f9-9342-5e0e6c6366aa.mp4" + }, + { + "id": "584063e4-31bc-4e16-bfd1-8e19e8291769", + "created_date": "2024-07-25 07:29:46.870579", + "last_modified_date": "2024-07-25 07:29:46.870579", + "version": 0, + "url": "https://ge.xhamster.com/videos/clothed-british-babes-give-head-xhXnnMO", + "review": 0, + "should_download": 0, + "title": "Bekleidete britische Sch\u00e4tzchen blasen | xHamster", + "file_name": "Bekleidete britische Sch\u00e4tzchen blasen [xhXnnMO].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/584063e4-31bc-4e16-bfd1-8e19e8291769.mp4" + }, + { + "id": "585137a1-4abe-4c62-9277-14e4acf8bbfb", + "created_date": "2024-07-25 07:29:45.098347", + "last_modified_date": "2024-07-25 07:29:45.098347", + "version": 0, + "url": "https://ge.xhamster.com/videos/taboo-family-sex-on-the-boat-11799368", + "review": 0, + "should_download": 0, + "title": "Tabu, Familiensex auf dem Boot | xHamster", + "file_name": "Tabu, Familiensex auf dem Boot [11799368].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/585137a1-4abe-4c62-9277-14e4acf8bbfb.mp4" + }, + { + "id": "58b23a74-a62c-4e93-b219-7a4fca14beec", + "created_date": "2024-07-25 07:29:45.464227", + "last_modified_date": "2024-07-25 07:29:45.464227", + "version": 0, + "url": "https://ge.xhamster.com/videos/geschichten-2-blut-schande-10248071", + "review": 0, + "should_download": 0, + "title": "Geschichten 2- Blut Schande, Free Porn Video f8 | xHamster", + "file_name": "Geschichten 2- Blut Schande [10248071].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/58b23a74-a62c-4e93-b219-7a4fca14beec.mp4" + }, + { + "id": "58cef52e-ad57-4d72-9e26-963ba9f8c6af", + "created_date": "2024-07-25 07:29:46.119204", + "last_modified_date": "2024-07-25 07:29:46.119204", + "version": 0, + "url": "https://ge.xhamster.com/videos/brattysis-katie-kush-to-stepbro-i-want-you-to-fuck-my-pie-s29-e7-xhV9wAY", + "review": 0, + "should_download": 0, + "title": "Brattysis \u2013 Katie kush f\u00fcr stiefbruer, \"ich will, dass du meinen kuchen fickst\" - s29: e7 | xHamster", + "file_name": "Brattysis \u2013 Katie kush f\u00fcr stiefbruer, \uff02ich will, dass du meinen kuchen fickst\uff02 - s29\uff1a e7 [xhV9wAY].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/58cef52e-ad57-4d72-9e26-963ba9f8c6af.mp4" + }, + { + "id": "59022dd4-709a-45c2-9fe6-04f90dfd2d91", + "created_date": "2024-07-25 07:29:47.141570", + "last_modified_date": "2024-07-25 07:29:47.141570", + "version": 0, + "url": "https://ge.xhamster.com/videos/party-has-three-lakefront-3459034", + "review": 0, + "should_download": 0, + "title": "Party hat drei am Seeufer | xHamster", + "file_name": "Party hat drei am Seeufer [3459034].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/59022dd4-709a-45c2-9fe6-04f90dfd2d91.mp4" + }, + { + "id": "591b5b7d-6afe-419e-8bf7-4af09527350a", + "created_date": "2024-07-25 07:29:46.550222", + "last_modified_date": "2024-07-25 07:29:46.550222", + "version": 0, + "url": "https://ge.xhamster.com/videos/step-moms-you-love-to-12799611", + "review": 0, + "should_download": 0, + "title": "Stiefmutter, die du liebst | xHamster", + "file_name": "Stiefmutter, die du liebst [12799611].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/591b5b7d-6afe-419e-8bf7-4af09527350a.mp4" + }, + { + "id": "5959b67b-96af-483f-888a-24539bc11751", + "created_date": "2024-07-25 07:29:47.942319", + "last_modified_date": "2024-07-25 07:29:47.942319", + "version": 0, + "url": "https://ge.xhamster.com/videos/vanilla-skye-gets-fucked-by-the-pool-2510051", + "review": 0, + "should_download": 0, + "title": "Vanilla Skye wird am Pool gefickt | xHamster", + "file_name": "Vanilla Skye wird am Pool gefickt [2510051].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5959b67b-96af-483f-888a-24539bc11751.mp4" + }, + { + "id": "5959f5dc-8566-4512-a46e-676121b26783", + "created_date": "2024-07-25 07:29:47.956670", + "last_modified_date": "2024-07-25 07:29:47.956670", + "version": 0, + "url": "https://ge.xhamster.com/videos/sex-project-full-movie-xh7tnc6", + "review": 0, + "should_download": 0, + "title": "Sex Project - kompletter film | xHamster", + "file_name": "Sex Project - kompletter film [xh7tnc6].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5959f5dc-8566-4512-a46e-676121b26783.mp4" + }, + { + "id": "595b08a2-faa6-4341-a8b4-3904e61be2c8", + "created_date": "2024-07-25 07:29:44.954569", + "last_modified_date": "2024-07-25 07:29:44.954569", + "version": 0, + "url": "https://ge.xhamster.com/videos/father-and-stepson-share-a-hot-blonde-teen-xhC8J2w", + "review": 0, + "should_download": 0, + "title": "Vater und Stiefsohn teilen sich ein hei\u00dfes blondes Teenie | xHamster", + "file_name": "Vater und Stiefsohn teilen sich ein hei\u00dfes blondes Teenie [xhC8J2w].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/595b08a2-faa6-4341-a8b4-3904e61be2c8.mp4" + }, + { + "id": "597198cb-d2fc-42c7-8c61-2c3e85a50bd4", + "created_date": "2024-07-25 07:29:45.700003", + "last_modified_date": "2024-07-25 07:29:45.700003", + "version": 0, + "url": "https://ge.xhamster.com/videos/taboo-italians-my-family-xhZPkBr", + "review": 0, + "should_download": 0, + "title": "Tabu-Italiener - meine Familie | xHamster", + "file_name": "Tabu-Italiener - meine Familie [xhZPkBr].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/597198cb-d2fc-42c7-8c61-2c3e85a50bd4.mp4" + }, + { + "id": "59849c2f-cf06-4d44-abdd-f22359f77276", + "created_date": "2024-07-25 07:29:46.169936", + "last_modified_date": "2024-07-25 07:29:46.169936", + "version": 0, + "url": "https://ge.xhamster.com/videos/tushy-blair-williams-has-a-hot-anal-lesson-threesome-8456320", + "review": 0, + "should_download": 0, + "title": "Tushy Blair Williams hat einen hei\u00dfen Anal-Dreier | xHamster", + "file_name": "Tushy Blair Williams hat einen hei\u00dfen Anal-Dreier [8456320].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/59849c2f-cf06-4d44-abdd-f22359f77276.mp4" + }, + { + "id": "59e64453-8745-4547-8fb3-def3afeeeabe", + "created_date": "2024-07-25 07:29:48.136085", + "last_modified_date": "2024-07-25 07:29:48.136085", + "version": 0, + "url": "https://ge.xhamster.com/videos/garage-girls-1981-6110405", + "review": 0, + "should_download": 0, + "title": "Garage Girls (1981) | xHamster", + "file_name": "Garage Girls (1981) [6110405].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/59e64453-8745-4547-8fb3-def3afeeeabe.mp4" + }, + { + "id": "5a2e2dd3-3078-4e31-8042-4ffbb005239e", + "created_date": "2024-09-24 08:11:39.004921", + "last_modified_date": "2024-10-21 16:28:44.680000", + "version": 1, + "url": "https://ge.xhamster.com/videos/shared-wife-with-step-daddys-friends-12336214", + "review": 0, + "should_download": 0, + "title": "Geteilte Ehefrau mit Stiefvaters Freunden | xHamster", + "file_name": "Geteilte Ehefrau mit Stiefvaters Freunden [12336214].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/5a2e2dd3-3078-4e31-8042-4ffbb005239e.mp4" + }, + { + "id": "5a326825-bab5-424b-a4f0-f3524f48b9d5", + "created_date": "2024-08-28 23:21:54.357799", + "last_modified_date": "2024-10-21 16:28:49.846000", + "version": 2, + "url": "https://ge.xhamster.com/videos/strip-poker-orgy-10775093", + "review": 0, + "should_download": 0, + "title": "Strip-Poker-Orgie | xHamster", + "file_name": "Strip-Poker-Orgie [10775093].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/5a326825-bab5-424b-a4f0-f3524f48b9d5.mp4" + }, + { + "id": "5a52e2dd-ad19-4ebf-9c01-1871d12a7150", + "created_date": "2024-07-25 07:29:45.854821", + "last_modified_date": "2024-07-25 07:29:45.854821", + "version": 0, + "url": "https://ge.xhamster.com/videos/sex-in-paradise-exploring-a-caribbean-island-and-having-sex-outdoors-beach-sex-outdoor-sex-swingers-teenage-couple-xhV0XcM", + "review": 0, + "should_download": 0, + "title": "Sex im Paradies, eine karibische Insel erkunden und Sex im Freien haben, Strandsex, Sex im Freien, Swinger-Teenager-Paar | xHamster", + "file_name": "Sex im Paradies, eine karibische Insel erkunden und Sex im Freien haben, Strandsex, Sex im Freien, Swinger-Teenager-Paar [xhV0XcM].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5a52e2dd-ad19-4ebf-9c01-1871d12a7150.mp4" + }, + { + "id": "5a7d4ee0-50f8-405e-8e15-99216b07ed32", + "created_date": "2024-07-25 07:29:45.112666", + "last_modified_date": "2024-07-25 07:29:45.112666", + "version": 0, + "url": "https://ge.xhamster.com/videos/american-family-secrets-chapter-05-xhuAfiH", + "review": 0, + "should_download": 0, + "title": "Amerikanische Familiengeheimnisse !!! - Kapitel # 05 | xHamster", + "file_name": "Amerikanische Familiengeheimnisse !!! - Kapitel # 05 [xhuAfiH].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5a7d4ee0-50f8-405e-8e15-99216b07ed32.mp4" + }, + { + "id": "5aa31eb9-c28d-4d64-9f02-9790d9af3f79", + "created_date": "2024-07-25 07:29:44.413437", + "last_modified_date": "2024-07-25 07:29:44.413437", + "version": 0, + "url": "https://ge.xhamster.com/videos/naturist-village-slutty-girl-feels-like-taking-a-strangers-cock-xhqLzCi", + "review": 0, + "should_download": 0, + "title": "Fkk village - ein versautes m\u00e4dchen f\u00fchlt sich, den schwanz eines fremden zu nehmen! | xHamster", + "file_name": "Fkk village - ein versautes m\u00e4dchen f\u00fchlt sich, den schwanz eines fremden zu nehmen! [xhqLzCi].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5aa31eb9-c28d-4d64-9f02-9790d9af3f79.mp4" + }, + { + "id": "5ab3147d-9bd8-4542-9691-46a97d0475cd", + "created_date": "2024-07-25 07:29:45.688814", + "last_modified_date": "2024-07-25 07:29:45.688814", + "version": 0, + "url": "https://ge.xhamster.com/videos/which-butthole-feels-better-stepmom-anal-challenge-abby-somers-xhryCzM", + "review": 0, + "should_download": 0, + "title": "Welches Arschloch f\u00fchlt sich besser an? Stiefmutter anal Herausforderung - Abby Somers | xHamster", + "file_name": "Welches Arschloch f\u00fchlt sich besser an\uff1f Stiefmutter anal Herausforderung - Abby Somers [xhryCzM].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5ab3147d-9bd8-4542-9691-46a97d0475cd.mp4" + }, + { + "id": "5b01bda7-be76-4a9e-8a2a-d5e233cfd95f", + "created_date": "2024-07-25 07:29:48.002746", + "last_modified_date": "2024-07-25 07:29:48.002746", + "version": 0, + "url": "https://ge.xhamster.com/videos/blindfolded-bride-gets-surprised-by-two-hard-cocks-at-once-xh6yaQD", + "review": 0, + "should_download": 0, + "title": "Die braut mit verbundenen augen wird von zwei harten schw\u00e4nze gleichzeitig \u00fcberrascht | xHamster", + "file_name": "Die braut mit verbundenen augen wird von zwei harten schw\u00e4nze gleichzeitig \u00fcberrascht [xh6yaQD].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5b01bda7-be76-4a9e-8a2a-d5e233cfd95f.mp4" + }, + { + "id": "5b1ddc83-ebf9-4b66-a069-b7b2e197d7d7", + "created_date": "2024-12-29 23:53:27.917609", + "last_modified_date": "2024-12-29 23:53:27.917609", + "version": 0, + "url": "https://ge.xhamster.com/videos/sex-spa-1984-12207796", + "review": 0, + "should_download": 0, + "title": "Sex Spa (1984) | xHamster", + "file_name": "Sex Spa (1984) [12207796].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/5b1ddc83-ebf9-4b66-a069-b7b2e197d7d7.mp4" + }, + { + "id": "5b34920c-6417-4b45-bcf1-de8c85f5d12d", + "created_date": "2024-07-25 07:29:47.077744", + "last_modified_date": "2025-01-03 11:56:26.627000", + "version": 1, + "url": "https://ge.xhamster.com/videos/the-family-magic-tea-turns-them-into-sluts-14174252", + "review": 0, + "should_download": 0, + "title": "Der magische Familientee macht sie zu Schlampen | xHamster", + "file_name": "Der magische Familientee macht sie zu Schlampen [14174252].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5b34920c-6417-4b45-bcf1-de8c85f5d12d.mp4" + }, + { + "id": "5b463837-1d9f-4f02-ade2-971add50ce45", + "created_date": "2024-07-25 07:29:47.979248", + "last_modified_date": "2024-09-06 09:40:21.347000", + "version": 1, + "url": "https://ge.xhamster.com/videos/my-friends-hot-mom-is-sara-jay-and-she-is-craving-some-young-cock-xhSoQ7o", + "review": 0, + "should_download": 0, + "title": "Die hei\u00dfe Mutter meiner Freundin ist Sara Jay und sie sehnt sich nach einem jungen Schwanz", + "file_name": "Die hei\u00dfe Mutter meiner Freundin ist Sara Jay und sie sehnt sich nach einem jungen Schwanz [xhSoQ7o].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5b463837-1d9f-4f02-ade2-971add50ce45.mp4" + }, + { + "id": "5b5c2827-0006-43de-a761-da5a01c86501", + "created_date": "2024-07-25 07:29:47.748803", + "last_modified_date": "2024-07-25 07:29:47.748803", + "version": 0, + "url": "https://ge.xhamster.com/videos/horny-couple-invites-hot-brunette-to-join-them-mp4-8598631", + "review": 0, + "should_download": 0, + "title": "Geiles Paar l\u00e4dt hei\u00dfe Br\u00fcnette ein, sich ihnen anzuschlie\u00dfen.mp4 | xHamster", + "file_name": "Geiles Paar l\u00e4dt hei\u00dfe Br\u00fcnette ein, sich ihnen anzuschlie\u00dfen.mp4 [8598631].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5b5c2827-0006-43de-a761-da5a01c86501.mp4" + }, + { + "id": "5b6dc84a-1989-4996-9c3e-48f36461337b", + "created_date": "2024-07-25 07:29:44.821483", + "last_modified_date": "2024-07-25 07:29:44.821483", + "version": 0, + "url": "https://ge.xhamster.com/videos/hot-german-mom-fucks-step-sons-friends-9538002", + "review": 0, + "should_download": 0, + "title": "Geile Mutti fickt die Freunde ihres Sohnes | xHamster", + "file_name": "Geile Mutti fickt die Freunde ihres Sohnes [9538002].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5b6dc84a-1989-4996-9c3e-48f36461337b.mp4" + }, + { + "id": "5bb6e870-903b-4179-bebf-3ad4b84a0a69", + "created_date": "2024-07-25 07:29:46.911750", + "last_modified_date": "2024-07-25 07:29:46.911750", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsis-says-dear-diary-i-bet-his-dick-gets-so-hard-xh20zjn", + "review": 0, + "should_download": 0, + "title": "Stiefschwester sagt, liebes Tagebuch, ich wette, sein Schwanz wird so hart! | xHamster", + "file_name": "Stiefschwester sagt, liebes Tagebuch, ich wette, sein Schwanz wird so hart! [xh20zjn].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5bb6e870-903b-4179-bebf-3ad4b84a0a69.mp4" + }, + { + "id": "5bc7490d-bd11-4dd9-801b-78b3ab4fd173", + "created_date": "2024-12-29 23:53:27.916641", + "last_modified_date": "2024-12-29 23:53:27.916641", + "version": 0, + "url": "https://ge.xhamster.com/videos/welcome-to-mommys-taboo-family-13073473", + "review": 0, + "should_download": 0, + "title": "Willkommen in Mamas Tabu-Familie | xHamster", + "file_name": "Willkommen in Mamas Tabu-Familie [13073473].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/5bc7490d-bd11-4dd9-801b-78b3ab4fd173.mp4" + }, + { + "id": "5c407737-f187-4f49-864c-df446b6b7054", + "created_date": "2024-07-25 07:29:47.371632", + "last_modified_date": "2024-07-25 07:29:47.371632", + "version": 0, + "url": "https://ge.xhamster.com/videos/dont-tell-mommy-please-2734429", + "review": 0, + "should_download": 0, + "title": "Sag es Mami bitte nicht | xHamster", + "file_name": "Sag es Mami bitte nicht [2734429].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5c407737-f187-4f49-864c-df446b6b7054.mp4" + }, + { + "id": "5c417968-7b72-4514-a391-f93c37d0a87a", + "created_date": "2024-07-25 07:29:45.433581", + "last_modified_date": "2024-07-25 07:29:45.433581", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-strokes-hot-stepsis-fucks-her-stepbro-in-the-shower-and-then-her-stepdad-fucks-her-harder-xhYdNOs", + "review": 0, + "should_download": 0, + "title": "Family Strokes - hei\u00dfe Stiefschwester fickt ihren Stiefbruder in der Dusche und dann fickt ihr Stiefvater sie h\u00e4rter | xHamster", + "file_name": "Family Strokes - hei\u00dfe Stiefschwester fickt ihren Stiefbruder in der Dusche und dann fickt ihr Stiefvater sie h\u00e4rter [xhYdNOs].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5c417968-7b72-4514-a391-f93c37d0a87a.mp4" + }, + { + "id": "5c9b629f-77e5-4ff2-ab3c-63d40658a915", + "created_date": "2024-07-25 07:29:44.606499", + "last_modified_date": "2024-07-25 07:29:44.606499", + "version": 0, + "url": "https://ge.xhamster.com/videos/german-taboo-7217495", + "review": 0, + "should_download": 0, + "title": "Deutsches Tabu | xHamster", + "file_name": "Deutsches Tabu [7217495].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5c9b629f-77e5-4ff2-ab3c-63d40658a915.mp4" + }, + { + "id": "5c9b787a-cc4f-408b-a890-4f558d7f4d26", + "created_date": "2024-07-25 07:29:47.398639", + "last_modified_date": "2024-07-25 07:29:47.398639", + "version": 0, + "url": "https://ge.xhamster.com/videos/sarah-and-friends-12-1991-sarah-young-german-dvd-rip-xhInACm", + "review": 0, + "should_download": 0, + "title": "Sarah und Freunde 12 (1991, Sarah Young, deutsch, DVD-Rip) | xHamster", + "file_name": "Sarah und Freunde 12 (1991, Sarah Young, deutsch, DVD-Rip) [xhInACm].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5c9b787a-cc4f-408b-a890-4f558d7f4d26.mp4" + }, + { + "id": "5c9f183e-e176-43e2-9518-5e26cf90bdc5", + "created_date": "2024-07-25 07:29:45.498483", + "last_modified_date": "2024-07-25 07:29:45.498483", + "version": 0, + "url": "https://ge.xhamster.com/videos/anal-threesome-at-the-beach-with-a-spanish-milf-and-a-busty-arab-girl-xh6Tvf0", + "review": 0, + "should_download": 0, + "title": "Analer dreier am strand mit einer spanischen MILf und einem vollbusigen arabischen m\u00e4dchen | xHamster", + "file_name": "Analer dreier am strand mit einer spanischen MILf und einem vollbusigen arabischen m\u00e4dchen [xh6Tvf0].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5c9f183e-e176-43e2-9518-5e26cf90bdc5.mp4" + }, + { + "id": "5d0fa5de-01d4-4240-b328-9386a1ef3e80", + "created_date": "2024-12-29 23:53:27.919442", + "last_modified_date": "2024-12-29 23:53:27.919442", + "version": 0, + "url": "https://ge.xhamster.com/videos/horny-household-part-3-stepdads-love-comforting-his-stepdaughter-with-his-big-dick-xhN5NxK9", + "review": 0, + "should_download": 0, + "title": "Geiler haushalt (teil 3): Stiefvaters liebe: tr\u00f6stet seine stieftochter mit seinem gro\u00dfen schwanz | xHamster", + "file_name": "Geiler haushalt (teil 3)\uff1a Stiefvaters liebe\uff1a tr\u00f6stet seine stieftochter mit seinem gro\u00dfen schwanz [xhN5NxK9].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/5d0fa5de-01d4-4240-b328-9386a1ef3e80.mp4" + }, + { + "id": "5d404b67-887f-476a-bab8-78fcad0ccdcb", + "created_date": "2024-07-25 07:29:47.001386", + "last_modified_date": "2024-07-25 07:29:47.001386", + "version": 0, + "url": "https://ge.xhamster.com/videos/wild-sex-on-the-beach-3763318", + "review": 0, + "should_download": 0, + "title": "Wilder Sex am Strand | xHamster", + "file_name": "Wilder Sex am Strand [3763318].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5d404b67-887f-476a-bab8-78fcad0ccdcb.mp4" + }, + { + "id": "5d7ed6f5-8617-4604-9d79-54793782dfed", + "created_date": "2024-07-25 07:29:44.551139", + "last_modified_date": "2024-07-25 07:29:44.551139", + "version": 0, + "url": "https://ge.xhamster.com/videos/heisse-braut-1989-dir-hans-billian-9976820", + "review": 0, + "should_download": 0, + "title": "Heisse Braut 1989 Dir Hans Billian, Free Porn 38 | xHamster", + "file_name": "Heisse Braut (1989) dir. Hans Billian [9976820].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5d7ed6f5-8617-4604-9d79-54793782dfed.mp4" + }, + { + "id": "5da6602a-e3a8-4de3-8737-0164c659f202", + "created_date": "2024-07-25 07:29:46.461603", + "last_modified_date": "2024-07-25 07:29:46.461603", + "version": 0, + "url": "https://ge.xhamster.com/videos/just-for-her-ass-xhuWt6Q", + "review": 0, + "should_download": 0, + "title": "Nur f\u00fcr ihren Arsch !!! | xHamster", + "file_name": "Nur f\u00fcr ihren Arsch !!! [xhuWt6Q].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5da6602a-e3a8-4de3-8737-0164c659f202.mp4" + }, + { + "id": "5da8c367-6908-4cce-9fad-2f0967c514f3", + "created_date": "2024-07-25 07:29:44.974236", + "last_modified_date": "2024-07-25 07:29:44.974236", + "version": 0, + "url": "https://ge.xhamster.com/videos/shy-teen-gets-used-by-stepdad-and-his-buddies-xhOZ7JQ", + "review": 0, + "should_download": 0, + "title": "Sch\u00fcchterner Teenager wird von Stiefvater und seinen Kumpels benutzt! | xHamster", + "file_name": "Sch\u00fcchterner Teenager wird von Stiefvater und seinen Kumpels benutzt! [xhOZ7JQ].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5da8c367-6908-4cce-9fad-2f0967c514f3.mp4" + }, + { + "id": "5daa7f54-2e86-4bfd-8b83-7910b8be9b01", + "created_date": "2024-12-29 23:53:27.889419", + "last_modified_date": "2024-12-29 23:53:27.889419", + "version": 0, + "url": "https://ge.xhamster.com/videos/frantic-fucking-featuring-patricia-rhomberg-xhFIqkG", + "review": 0, + "should_download": 0, + "title": "Hektisches Ficken mit Patricia Rhomberg | xHamster", + "file_name": "Hektisches Ficken mit Patricia Rhomberg [xhFIqkG].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/5daa7f54-2e86-4bfd-8b83-7910b8be9b01.mp4" + }, + { + "id": "5dd55acb-053b-4d69-a256-e9843390a574", + "created_date": "2024-09-24 08:11:39.005295", + "last_modified_date": "2024-10-21 16:28:56.160000", + "version": 1, + "url": "https://ge.xhamster.com/videos/amateur-cheating-fuck-while-calling-her-boyfriend-german-teen-nicky-foxx-xhUZmay", + "review": 0, + "should_download": 0, + "title": "Fremder fickt sie und Freund ist am Telefon - Deutsche Nicky-Foxx beim Hotel Date | xHamster", + "file_name": "Fremder fickt sie und Freund ist am Telefon - Deutsche Nicky-Foxx beim Hotel Date [xhUZmay].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/5dd55acb-053b-4d69-a256-e9843390a574.mp4" + }, + { + "id": "5de3213d-e226-474d-acfb-e21e5c75ee1e", + "created_date": "2024-07-25 07:29:45.575197", + "last_modified_date": "2024-07-25 07:29:45.575197", + "version": 0, + "url": "https://ge.xhamster.com/videos/big-tit-whorehouse-7811453", + "review": 0, + "should_download": 0, + "title": "Vollbusiges Haus mit dicken Titten | xHamster", + "file_name": "Vollbusiges Haus mit dicken Titten [7811453].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5de3213d-e226-474d-acfb-e21e5c75ee1e.mp4" + }, + { + "id": "5df6de1c-be0c-4528-8acf-f8c1cdb0ffa6", + "created_date": "2024-07-25 07:29:44.424890", + "last_modified_date": "2024-07-25 07:29:44.424890", + "version": 0, + "url": "https://ge.xhamster.com/videos/familiensunden-unter-deutschen-dachern-15-11280871", + "review": 0, + "should_download": 0, + "title": "Familiensunden Unter Deutschen Dachern 15: Free Porn 48 | xHamster", + "file_name": "FAMILIENSUNDEN UNTER DEUTSCHEN DACHERN 15 [11280871].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5df6de1c-be0c-4528-8acf-f8c1cdb0ffa6.mp4" + }, + { + "id": "5e314554-b1c0-4e02-9da0-ed1db040109c", + "created_date": "2024-07-25 07:29:44.559139", + "last_modified_date": "2024-07-25 07:29:44.559139", + "version": 0, + "url": "https://ge.xhamster.com/videos/we-fuck-because-we-care-for-each-other-xhrMO3X", + "review": 0, + "should_download": 0, + "title": "Wir ficken, weil wir uns umeinander k\u00fcmmern | xHamster", + "file_name": "Wir ficken, weil wir uns umeinander k\u00fcmmern [xhrMO3X].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5e314554-b1c0-4e02-9da0-ed1db040109c.mp4" + }, + { + "id": "5e4bdcdb-fc0c-4d5a-9657-93cbad398aba", + "created_date": "2024-07-25 07:29:46.226481", + "last_modified_date": "2024-07-25 07:29:46.226481", + "version": 0, + "url": "https://ge.xhamster.com/videos/dasdys-home-3-full-movie-8509530", + "review": 0, + "should_download": 0, + "title": "Dasdys Zuhause 3, kompletter Film | xHamster", + "file_name": "Dasdys Zuhause 3, kompletter Film [8509530].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5e4bdcdb-fc0c-4d5a-9657-93cbad398aba.mp4" + }, + { + "id": "5e5a9fba-716d-41ed-b078-b7125af93a35", + "created_date": "2024-07-25 07:29:44.451331", + "last_modified_date": "2024-07-25 07:29:44.451331", + "version": 0, + "url": "https://ge.xhamster.com/videos/gesellschaftsspiele-1979-5765346", + "review": 0, + "should_download": 0, + "title": "Gesellschaftsspiele - 1979, Free Party Porn 75 | xHamster", + "file_name": "Gesellschaftsspiele - 1979 [5765346].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5e5a9fba-716d-41ed-b078-b7125af93a35.mp4" + }, + { + "id": "5e60f5d4-324b-4501-ad03-7758b3de745f", + "created_date": "2024-07-25 07:29:46.974788", + "last_modified_date": "2024-07-25 07:29:46.974788", + "version": 0, + "url": "https://ge.xhamster.com/videos/apartment-episode-3-xhZq3hQ", + "review": 0, + "should_download": 0, + "title": "Wohnung - Episode 3 | xHamster", + "file_name": "Wohnung - Episode 3 [xhZq3hQ].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5e60f5d4-324b-4501-ad03-7758b3de745f.mp4" + }, + { + "id": "5e706b05-2b91-474d-bd4a-92ee11578273", + "created_date": "2024-07-25 07:29:46.084657", + "last_modified_date": "2024-07-25 07:29:46.084657", + "version": 0, + "url": "https://ge.xhamster.com/videos/er-hat-mit-ihr-geilen-sex-auf-der-couch-xhDnyXN", + "review": 0, + "should_download": 0, + "title": "Er Hat Mit Ihr Geilen Sex Auf Der Couch, Porn 5f | xHamster", + "file_name": "Er hat mit Ihr geilen Sex auf der Couch [xhDnyXN].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5e706b05-2b91-474d-bd4a-92ee11578273.mp4" + }, + { + "id": "5e7a8581-9f05-409f-9aee-12c1896b22da", + "created_date": "2024-07-25 07:29:46.395500", + "last_modified_date": "2024-07-25 07:29:46.395500", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsister-and-bestie-give-cock-a-helping-hand-s4-e6-10523919", + "review": 0, + "should_download": 0, + "title": "Stiefschwester und Bestie helfen dem Schwanz s4: e6 | xHamster", + "file_name": "Stiefschwester und Bestie helfen dem Schwanz s4\uff1a e6 [10523919].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5e7a8581-9f05-409f-9aee-12c1896b22da.mp4" + }, + { + "id": "5e906951-4cd9-4fd4-a10f-27640c719259", + "created_date": "2024-07-25 07:29:47.447121", + "last_modified_date": "2024-07-25 07:29:47.447121", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsis-says-i-dont-know-if-youve-always-wanted-a-sister-or-if-you-just-want-to-stick-your-dick-in-my-tight-pussy-xhs2ZhF", + "review": 0, + "should_download": 0, + "title": "Stiefschwester sagt, ich wei\u00df nicht, ob du immer eine Schwester wolltest oder ob du deinen Schwanz einfach in meine enge Muschi stecken willst | xHamster", + "file_name": "Stiefschwester sagt, ich wei\u00df nicht, ob du immer eine Schwester wolltest oder ob du deinen Schwanz einfach in meine enge Muschi stecken willst [xhs2ZhF].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5e906951-4cd9-4fd4-a10f-27640c719259.mp4" + }, + { + "id": "5ea44d59-36ea-48c3-93e0-ca18fe52ce1b", + "created_date": "2024-07-25 07:29:45.816867", + "last_modified_date": "2024-07-25 07:29:45.816867", + "version": 0, + "url": "https://ge.xhamster.com/videos/sisters-1979-10432209", + "review": 0, + "should_download": 0, + "title": "Schwestern (1979) | xHamster", + "file_name": "Schwestern (1979) [10432209].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5ea44d59-36ea-48c3-93e0-ca18fe52ce1b.mp4" + }, + { + "id": "5f4a4cf8-d0f6-47e6-9767-7f950ca03ad9", + "created_date": "2024-07-25 07:29:46.366257", + "last_modified_date": "2024-07-25 07:29:46.366257", + "version": 0, + "url": "https://ge.xhamster.com/videos/his-dick-is-huge-i-just-want-to-see-it-tough-love-3some-13375985", + "review": 0, + "should_download": 0, + "title": "Sein Schwanz ist riesig, ich will es nur sehen - harte Liebe, Dreier | xHamster", + "file_name": "Sein Schwanz ist riesig, ich will es nur sehen - harte Liebe, Dreier [13375985].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5f4a4cf8-d0f6-47e6-9767-7f950ca03ad9.mp4" + }, + { + "id": "5f4f8175-93b7-4235-98d3-3d7c223bf587", + "created_date": "2024-07-25 07:29:47.206268", + "last_modified_date": "2024-07-25 07:29:47.206268", + "version": 0, + "url": "https://ge.xhamster.com/videos/hot-group-sex-on-a-public-spanish-beach-xhYvrzR", + "review": 0, + "should_download": 0, + "title": "Hei\u00dfer Gruppensex an einem \u00f6ffentlichen spanischen Strand | xHamster", + "file_name": "Hei\u00dfer Gruppensex an einem \u00f6ffentlichen spanischen Strand [xhYvrzR].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5f4f8175-93b7-4235-98d3-3d7c223bf587.mp4" + }, + { + "id": "5f53107e-4a25-45fd-abd1-c00b075833f2", + "created_date": "2024-07-25 07:29:44.760616", + "last_modified_date": "2024-07-25 07:29:44.760616", + "version": 0, + "url": "https://ge.xhamster.com/videos/dare-ring-game-11-xh0wXh0", + "review": 0, + "should_download": 0, + "title": "Dare-Ring-Spiel 11 | xHamster", + "file_name": "Dare-Ring-Spiel 11 [xh0wXh0].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5f53107e-4a25-45fd-abd1-c00b075833f2.mp4" + }, + { + "id": "5f6e1fc0-2430-41b0-813a-a53ed494a464", + "created_date": "2024-07-25 07:29:45.371612", + "last_modified_date": "2024-07-25 07:29:45.371612", + "version": 0, + "url": "https://ge.xhamster.com/videos/im-tasting-the-hot-juices-from-my-horny-mummys-cunt-stepmother-and-stepdaughter-xhyeR33", + "review": 0, + "should_download": 0, + "title": "Ich probiere die hei\u00dfen S\u00e4fte aus der Fotze meiner geilen Mami! Stiefmutter und Stieftochter! | xHamster", + "file_name": "Ich probiere die hei\u00dfen S\u00e4fte aus der Fotze meiner geilen Mami! Stiefmutter und Stieftochter! [xhyeR33].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/5f6e1fc0-2430-41b0-813a-a53ed494a464.mp4" + }, + { + "id": "5f8b2177-2862-4e38-a9e2-9a925657e46b", + "created_date": "2024-07-25 07:29:46.663107", + "last_modified_date": "2024-07-25 07:29:46.663107", + "version": 0, + "url": "https://ge.xhamster.com/videos/wanna-taste-your-stepbros-cum-lulu-chu-asks-maria-kazi-s25-e12-xhFkD8i", + "review": 0, + "should_download": 0, + "title": "\"willst du das sperma deines stiefbruers probieren?\" Lulu Chu fragt Maria Kazi- S25: E12 | xHamster", + "file_name": "\uff02willst du das sperma deines stiefbruers probieren\uff1f\uff02 Lulu Chu fragt Maria Kazi- S25\uff1a E12 [xhFkD8i].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/5f8b2177-2862-4e38-a9e2-9a925657e46b.mp4" + }, + { + "id": "6043593a-0e4c-4fb0-9b08-89c23d92fdf7", + "created_date": "2024-07-25 07:29:44.602877", + "last_modified_date": "2024-07-25 07:29:44.602877", + "version": 0, + "url": "https://ge.xhamster.com/videos/bratty-sis-slutty-sisters-fight-over-step-dad-s-cock-8469724", + "review": 0, + "should_download": 0, + "title": "Bratty Sis - versaute Schwestern k\u00e4mpfen um den Schwanz von Stiefvater | xHamster", + "file_name": "Bratty Sis - versaute Schwestern k\u00e4mpfen um den Schwanz von Stiefvater [8469724].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6043593a-0e4c-4fb0-9b08-89c23d92fdf7.mp4" + }, + { + "id": "606db219-6651-4e0a-8dc3-78f374f24b58", + "created_date": "2024-07-25 07:29:44.374238", + "last_modified_date": "2024-07-25 07:29:44.374238", + "version": 0, + "url": "https://ge.xhamster.com/videos/fam-63-xh2dim3", + "review": 0, + "should_download": 0, + "title": "Fam 63: Free Porn Video 6b | xHamster", + "file_name": "fam 63 [xh2dim3].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/606db219-6651-4e0a-8dc3-78f374f24b58.mp4" + }, + { + "id": "60896198-1e31-4730-b896-eeead1e206fb", + "created_date": "2024-07-25 07:29:48.118471", + "last_modified_date": "2024-07-25 07:29:48.118471", + "version": 0, + "url": "https://ge.xhamster.com/videos/creamed-stepsis-pussy-while-step-dad-sleeps-s9-e2-10936147", + "review": 0, + "should_download": 0, + "title": "Sahnige Stiefschwester-Muschi gesahnt, w\u00e4hrend Stiefvater s9: e2 schl\u00e4ft | xHamster", + "file_name": "Sahnige Stiefschwester-Muschi gesahnt, w\u00e4hrend Stiefvater s9\uff1a e2 schl\u00e4ft [10936147].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/60896198-1e31-4730-b896-eeead1e206fb.mp4" + }, + { + "id": "60ab814e-7b3e-4a15-94d6-5b61b3392969", + "created_date": "2024-07-25 07:29:48.017410", + "last_modified_date": "2024-07-25 07:29:48.017410", + "version": 0, + "url": "https://ge.xhamster.com/videos/18-and-confused-6-xhv0Cil", + "review": 0, + "should_download": 0, + "title": "18 und verwirrt # 6 | xHamster", + "file_name": "18 und verwirrt # 6 [xhv0Cil].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/60ab814e-7b3e-4a15-94d6-5b61b3392969.mp4" + }, + { + "id": "60c70b0e-82a6-4727-97a1-ad1c3db3b6f8", + "created_date": "2024-07-25 07:29:48.147842", + "last_modified_date": "2024-07-25 07:29:48.147842", + "version": 0, + "url": "https://ge.xhamster.com/videos/teeny-express-13221400", + "review": 0, + "should_download": 0, + "title": "Teeny-Express | xHamster", + "file_name": "Teeny-Express [13221400].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/60c70b0e-82a6-4727-97a1-ad1c3db3b6f8.mp4" + }, + { + "id": "60c8dea2-91b6-4bb3-8060-cc0ebbc3e015", + "created_date": "2024-07-25 07:29:46.045811", + "last_modified_date": "2024-07-25 07:29:46.045811", + "version": 0, + "url": "https://ge.xhamster.com/videos/panties-on-fire-1979-9357764", + "review": 0, + "should_download": 0, + "title": "H\u00f6schen in Flammen (1979) | xHamster", + "file_name": "H\u00f6schen in Flammen (1979) [9357764].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/60c8dea2-91b6-4bb3-8060-cc0ebbc3e015.mp4" + }, + { + "id": "60d91fe5-8e74-48d7-add7-7a67089a2a93", + "created_date": "2024-07-25 07:29:46.130104", + "last_modified_date": "2024-07-25 07:29:46.130104", + "version": 0, + "url": "https://ge.xhamster.com/videos/sex-orgy-830-10963835", + "review": 0, + "should_download": 0, + "title": "Sexorgie 830 | xHamster", + "file_name": "Sexorgie 830 [10963835].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/60d91fe5-8e74-48d7-add7-7a67089a2a93.mp4" + }, + { + "id": "6149f5a7-f33d-4c08-a922-d04fc88f4880", + "created_date": "2024-07-25 07:29:45.579826", + "last_modified_date": "2024-07-25 07:29:45.579826", + "version": 0, + "url": "https://ge.xhamster.com/videos/familie-matuschek-die-verfickte-hochzeit-avi-mp4-openload-8309689", + "review": 0, + "should_download": 0, + "title": "Familie Matuschek - Die Verfickte Hochzeit Avi Mp4 Openload | xHamster", + "file_name": "Familie Matuschek - Die verfickte Hochzeit.avi.mp4 openload. [8309689].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/6149f5a7-f33d-4c08-a922-d04fc88f4880.mp4" + }, + { + "id": "61a80611-e7f1-49b6-b222-60c7c54ba745", + "created_date": "2024-07-25 07:29:44.842853", + "last_modified_date": "2024-07-25 07:29:44.842853", + "version": 0, + "url": "https://ge.xhamster.com/videos/mutual-masturbation-with-neighbour-while-wife-is-watching-cumshot-programmerswife-xhKvype", + "review": 0, + "should_download": 0, + "title": "Gegenseitige masturbation mit dem nachbarn, w\u00e4hrend ehefrau zuschaut, abspritzen - ProgrammersWife | xHamster", + "file_name": "Gegenseitige masturbation mit dem nachbarn, w\u00e4hrend ehefrau zuschaut, abspritzen - ProgrammersWife [xhKvype].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/61a80611-e7f1-49b6-b222-60c7c54ba745.mp4" + }, + { + "id": "61b149ad-b830-4de1-be29-43ee3fa5f3c0", + "created_date": "2024-07-25 07:29:46.383126", + "last_modified_date": "2024-07-25 07:29:46.383126", + "version": 0, + "url": "https://ge.xhamster.com/videos/mother-and-daughter-decide-to-get-down-for-some-family-fun-7894256", + "review": 0, + "should_download": 0, + "title": "Mutter und Tochter beschlie\u00dfen, f\u00fcr etwas Familienspa\u00df runterzukommen | xHamster", + "file_name": "Mutter und Tochter beschlie\u00dfen, f\u00fcr etwas Familienspa\u00df runterzukommen [7894256].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/61b149ad-b830-4de1-be29-43ee3fa5f3c0.mp4" + }, + { + "id": "61b43c4f-4f94-4ba7-a7e5-87b9675e2196", + "created_date": "2024-12-29 23:53:27.883229", + "last_modified_date": "2025-01-03 00:49:09.828000", + "version": 3, + "url": "https://ge.xhamster.com/videos/vixen-petite-cute-young-intern-coco-seduces-her-older-boss-xh9Yz38", + "review": 0, + "should_download": 0, + "title": "Vixen - die zierliche, s\u00fc\u00dfe junge Praktikantin Coco verf\u00fchrt ihren \u00e4lteren Chef | xHamster", + "file_name": "Vixen - die zierliche, s\u00fc\u00dfe junge Praktikantin Coco verf\u00fchrt ihren \u00e4lteren Chef [xh9Yz38].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/61b43c4f-4f94-4ba7-a7e5-87b9675e2196.mp4" + }, + { + "id": "61b4b5d1-5284-4515-9fba-35878b0dcf9d", + "created_date": "2024-07-25 07:29:47.055261", + "last_modified_date": "2024-07-25 07:29:47.055261", + "version": 0, + "url": "https://ge.xhamster.com/videos/busty-teen-with-perfect-ass-blair-hudson-visits-her-free-use-step-aunt-step-uncle-familystrokes-xhgJ2os", + "review": 0, + "should_download": 0, + "title": "Vollbusiges teen mit perfektem arsch blair hudson besucht ihre kostenlose stieftante & stiefsohn-onkel, familystrokes | xHamster", + "file_name": "Vollbusiges teen mit perfektem arsch blair hudson besucht ihre kostenlose stieftante & stiefsohn-onkel, familystrokes [xhgJ2os].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/61b4b5d1-5284-4515-9fba-35878b0dcf9d.mp4" + }, + { + "id": "61c06fde-c3db-40cc-b82a-5072c75ad26a", + "created_date": "2024-12-29 23:53:27.924415", + "last_modified_date": "2024-12-29 23:53:27.924415", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-mischiefs-part-2-bf-wants-to-go-again-creampies-gf-while-her-step-moms-in-the-shower-xhvVr4i", + "review": 0, + "should_download": 0, + "title": "Familien-unfug (teil 2): freund will wieder gehen! Typ spritzt freundin voll, w\u00e4hrend ihre stiefmutter unter der dusche ist | xHamster", + "file_name": "Familien-unfug (teil 2)\uff1a freund will wieder gehen! Typ spritzt freundin voll, w\u00e4hrend ihre stiefmutter unter der dusche ist [xhvVr4i].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/61c06fde-c3db-40cc-b82a-5072c75ad26a.mp4" + }, + { + "id": "61ddca63-0dc5-4c7b-9250-e543d814085b", + "created_date": "2024-09-24 08:11:38.999581", + "last_modified_date": "2024-10-21 16:29:04.143000", + "version": 1, + "url": "https://ge.xhamster.com/videos/mommys-boy-alison-rey-learns-how-to-fuck-with-her-boyfriends-stepmoms-help-ffm-threesome-xh5HrfI", + "review": 0, + "should_download": 0, + "title": "Mamas Junge - Alison Rey lernt mit Hilfe der Stiefmutter ihres Freundes zu ficken! ffm Dreier! | xHamster", + "file_name": "Mamas Junge - Alison Rey lernt mit Hilfe der Stiefmutter ihres Freundes zu ficken! ffm Dreier! [xh5HrfI].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/61ddca63-0dc5-4c7b-9250-e543d814085b.mp4" + }, + { + "id": "622de32e-8bb0-4c76-a2fc-f7bb90914970", + "created_date": "2024-07-25 07:29:47.934225", + "last_modified_date": "2024-07-25 07:29:47.934225", + "version": 0, + "url": "https://ge.xhamster.com/videos/i-wasnt-planning-on-having-sex-with-my-step-son-xhZ5hg9", + "review": 0, + "should_download": 0, + "title": "Ich hatte nicht vor, Sex mit meinem Stiefsohn zu haben | xHamster", + "file_name": "Ich hatte nicht vor, Sex mit meinem Stiefsohn zu haben [xhZ5hg9].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/622de32e-8bb0-4c76-a2fc-f7bb90914970.mp4" + }, + { + "id": "629deea6-a275-4900-b439-fe7e946c2c72", + "created_date": "2024-08-09 20:22:02.076793", + "last_modified_date": "2024-08-16 10:30:54.797000", + "version": 1, + "url": "https://ge.xhamster.com/videos/my-hot-redhead-gf-shares-my-cock-with-her-bff-sonia-harcourt-jay-taylor-xh7x3cJ", + "review": 0, + "should_download": 0, + "title": "Meine hei\u00dfe rothaarige freundin teilt meinen schwanz mit ihrem besten freund - sonia harcourt & jay taylor | xHamster", + "file_name": "Meine hei\u00dfe rothaarige freundin teilt meinen schwanz mit ihrem besten freund - sonia harcourt & jay taylor [xh7x3cJ].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/629deea6-a275-4900-b439-fe7e946c2c72.mp4" + }, + { + "id": "62ad174e-ae76-495a-bf8d-5099e4dafa41", + "created_date": "2024-07-25 07:29:46.904180", + "last_modified_date": "2024-07-25 07:29:46.904180", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-swap-ready-set-kiss-with-sharon-white-bonnie-dolce-s7-e6-xhCsqcx", + "review": 0, + "should_download": 0, + "title": "Familientausch: bereit, gesetzt, KISS - mit sharon white & bonnie dolce- s7: e6 | xHamster", + "file_name": "Familientausch\uff1a bereit, gesetzt, KISS - mit sharon white & bonnie dolce- s7\uff1a e6 [xhCsqcx].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/62ad174e-ae76-495a-bf8d-5099e4dafa41.mp4" + }, + { + "id": "62cff9b0-fe6a-4301-b091-46e5951b6b77", + "created_date": "2024-07-25 07:29:45.595114", + "last_modified_date": "2024-07-25 07:29:45.595114", + "version": 0, + "url": "https://ge.xhamster.com/videos/daddy-cool-1999-xhthmkq", + "review": 0, + "should_download": 0, + "title": "Papi ist cool, 1999 | xHamster", + "file_name": "Papi ist cool, 1999 [xhthmkq].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/62cff9b0-fe6a-4301-b091-46e5951b6b77.mp4" + }, + { + "id": "62f854c5-2695-4b68-b32a-dd7ca6f93a01", + "created_date": "2024-07-25 07:29:47.194934", + "last_modified_date": "2024-07-25 07:29:47.194934", + "version": 0, + "url": "https://ge.xhamster.com/videos/das-haus-der-geheimen-luste-1979-xhwJoJs", + "review": 0, + "should_download": 0, + "title": "Das Haus Der Geheimen Luste 1979, Free Porn ae | xHamster", + "file_name": "Das Haus Der Geheimen Luste ( 1979 ) [xhwJoJs].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/62f854c5-2695-4b68-b32a-dd7ca6f93a01.mp4" + }, + { + "id": "633708a4-f248-4361-8f7c-97e6b29e5d90", + "created_date": "2024-07-25 07:29:45.456831", + "last_modified_date": "2024-07-25 07:29:45.456831", + "version": 0, + "url": "https://ge.xhamster.com/videos/innocent-teens-become-public-pussy-eating-sluts-14050190", + "review": 0, + "should_download": 0, + "title": "Unschuldige Teenager werden \u00f6ffentlich Muschi-essende Schlampen | xHamster", + "file_name": "Unschuldige Teenager werden \u00f6ffentlich Muschi-essende Schlampen [14050190].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/633708a4-f248-4361-8f7c-97e6b29e5d90.mp4" + }, + { + "id": "6362f8c6-2b1d-43e4-b246-a3289a509cf8", + "created_date": "2024-07-25 07:29:47.911108", + "last_modified_date": "2024-07-25 07:29:47.911108", + "version": 0, + "url": "https://ge.xhamster.com/videos/hot-step-aunt-thinks-shes-cool-and-lets-me-fuck-her-ass-jordan-maxx-xh4FVDA", + "review": 0, + "should_download": 0, + "title": "Hei\u00dfe Stieftante h\u00e4lt sie f\u00fcr cool und l\u00e4sst mich ihren Arsch ficken - Jordan Maxx | xHamster", + "file_name": "Hei\u00dfe Stieftante h\u00e4lt sie f\u00fcr cool und l\u00e4sst mich ihren Arsch ficken - Jordan Maxx [xh4FVDA].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6362f8c6-2b1d-43e4-b246-a3289a509cf8.mp4" + }, + { + "id": "63636ea1-a64f-41f3-a43f-fd4bbab0a3bf", + "created_date": "2024-07-25 07:29:44.628400", + "last_modified_date": "2024-07-25 07:29:44.628400", + "version": 0, + "url": "https://ge.xhamster.com/videos/german-classic-2964283", + "review": 0, + "should_download": 0, + "title": "Deutscher Klassiker | xHamster", + "file_name": "Deutscher Klassiker [2964283].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/63636ea1-a64f-41f3-a43f-fd4bbab0a3bf.mp4" + }, + { + "id": "6455f8dc-d334-4718-8293-e879e28a7e11", + "created_date": "2025-01-05 14:37:11.046000", + "last_modified_date": "2025-01-05 14:40:21.685000", + "version": 1, + "url": "https://ge.xhamster.com/videos/couple-caught-in-the-act-by-friends-11871326", + "review": 0, + "should_download": 0, + "title": "Paar von Freunden erwischt | xHamster", + "file_name": "Paar von Freunden erwischt [11871326].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/6455f8dc-d334-4718-8293-e879e28a7e11.mp4" + }, + { + "id": "649e9cb6-4252-4093-a9ae-36f13e74da62", + "created_date": "2024-07-25 07:29:47.322945", + "last_modified_date": "2024-07-25 07:29:47.322945", + "version": 0, + "url": "https://ge.xhamster.com/videos/die-traumfrau-xhXWPzp", + "review": 0, + "should_download": 0, + "title": "Die Traumfrau: Free European HD Porn Video 9a | xHamster", + "file_name": "Die Traumfrau [xhXWPzp].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/649e9cb6-4252-4093-a9ae-36f13e74da62.mp4" + }, + { + "id": "64e26069-eb61-432a-892b-83adbe6abacb", + "created_date": "2024-07-25 07:29:47.063358", + "last_modified_date": "2024-07-25 07:29:47.063358", + "version": 0, + "url": "https://ge.xhamster.com/videos/fa-cute-schoolgirl-gets-the-help-she-wanted-13904739", + "review": 0, + "should_download": 0, + "title": "Fa, s\u00fc\u00dfes Schulm\u00e4dchen bekommt die Hilfe, die sie wollte! | xHamster", + "file_name": "Fa, s\u00fc\u00dfes Schulm\u00e4dchen bekommt die Hilfe, die sie wollte! [13904739].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/64e26069-eb61-432a-892b-83adbe6abacb.mp4" + }, + { + "id": "64f0fc42-17f6-48d4-a1b6-a97ec197a25b", + "created_date": "2024-07-25 07:29:45.835813", + "last_modified_date": "2024-07-25 07:29:45.835813", + "version": 0, + "url": "https://ge.xhamster.com/videos/josefine-mutzenbacher-the-whore-of-vienna-xh7Gj8r", + "review": 0, + "should_download": 0, + "title": "Josefine Mutzenbacher Die Hure Von Wien | xHamster", + "file_name": "Josefine Mutzenbacher Die Hure Von Wien [xh7Gj8r].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/64f0fc42-17f6-48d4-a1b6-a97ec197a25b.mp4" + }, + { + "id": "6508e9a7-ca3b-4be7-b177-3ac17272facd", + "created_date": "2024-07-25 07:29:46.411730", + "last_modified_date": "2024-07-25 07:29:46.411730", + "version": 0, + "url": "https://ge.xhamster.com/videos/der-perfekte-angestellte-nimmt-es-in-den-arsch-full-movie-xhmyyMG", + "review": 0, + "should_download": 0, + "title": "Der Perfekte Angestellte Nimmt Es in Den Arsch Full Movie | xHamster", + "file_name": "Der perfekte Angestellte nimmt es in den Arsch (Full Movie) [xhmyyMG].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6508e9a7-ca3b-4be7-b177-3ac17272facd.mp4" + }, + { + "id": "654ce8a0-807e-4b68-8356-87903642bd5a", + "created_date": "2024-10-21 15:08:43.550005", + "last_modified_date": "2024-10-21 16:29:09.478000", + "version": 1, + "url": "https://ge.xhamster.com/videos/hot-secretary-gets-fucked-by-three-horny-co-workers-xhaBfNF", + "review": 0, + "should_download": 0, + "title": "Hei\u00dfe sekret\u00e4rin wird von drei geilen kollegen gefickt | xHamster", + "file_name": "Hei\u00dfe sekret\u00e4rin wird von drei geilen kollegen gefickt [xhaBfNF].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/654ce8a0-807e-4b68-8356-87903642bd5a.mp4" + }, + { + "id": "65cd77d4-b124-4dce-9098-25f16714cd32", + "created_date": "2024-07-25 07:29:46.715263", + "last_modified_date": "2024-07-25 07:29:46.715263", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-cousins-like-anal-creampies-full-hd-movie-original-xhTCHpH", + "review": 0, + "should_download": 0, + "title": "Meine Cousins m\u00f6gen anal Creampies - (Full HD-Film - Original) | xHamster", + "file_name": "Meine Cousins m\u00f6gen anal Creampies - (Full HD-Film - Original) [xhTCHpH].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/65cd77d4-b124-4dce-9098-25f16714cd32.mp4" + }, + { + "id": "65d66794-a62f-4cae-bc10-11c120f186df", + "created_date": "2024-07-25 07:29:47.805084", + "last_modified_date": "2024-07-25 07:29:47.805084", + "version": 0, + "url": "https://ge.xhamster.com/videos/tabooheat-theres-enough-cory-chase-for-all-her-sneaky-stepfamily-xhejEOr", + "review": 0, + "should_download": 0, + "title": "Tabooheat \u2013 Es gibt genug cory Chase f\u00fcr all ihre hinterh\u00e4ltige stieffamilie! | xHamster", + "file_name": "Tabooheat \u2013 Es gibt genug cory Chase f\u00fcr all ihre hinterh\u00e4ltige stieffamilie! [xhejEOr].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/65d66794-a62f-4cae-bc10-11c120f186df.mp4" + }, + { + "id": "6616a23b-436e-4b2f-82b6-9941e07f96f6", + "created_date": "2025-01-05 14:36:11.957000", + "last_modified_date": "2025-01-05 14:36:38.381000", + "version": 1, + "url": "https://ge.xhamster.com/videos/familystrokes-gorgeous-cutie-offers-all-her-holes-to-appease-stepbros-for-all-her-wrong-doings-xhr6HZ3", + "review": 0, + "should_download": 0, + "title": "Familystrokes - die wundersch\u00f6ne S\u00fc\u00dfe bietet alle ihre L\u00f6cher an, um Stiefbruder f\u00fcr all ihre falschen Taten zu bes\u00e4nftigen | xHamster", + "file_name": "Familystrokes - die wundersch\u00f6ne S\u00fc\u00dfe bietet alle ihre L\u00f6cher an, um Stiefbruder f\u00fcr all ihre falschen Taten zu bes\u00e4nftigen [xhr6HZ3].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/6616a23b-436e-4b2f-82b6-9941e07f96f6.mp4" + }, + { + "id": "663478bd-89ac-4a83-bc1d-87c3a34ec485", + "created_date": "2024-07-25 07:29:45.072108", + "last_modified_date": "2024-07-25 07:29:45.072108", + "version": 0, + "url": "https://ge.xhamster.com/videos/kinky-family-blair-williams-fucking-my-hot-big-ass-steps-8799232", + "review": 0, + "should_download": 0, + "title": "Versaute Familie - Blair Williams - fickt meine hei\u00dfen Schritte mit dickem Arsch | xHamster", + "file_name": "Versaute Familie - Blair Williams - fickt meine hei\u00dfen Schritte mit dickem Arsch [8799232].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/663478bd-89ac-4a83-bc1d-87c3a34ec485.mp4" + }, + { + "id": "663e820a-b8d1-4711-8dd8-78a1476e4d1d", + "created_date": "2024-07-25 07:29:44.462307", + "last_modified_date": "2024-07-25 07:29:44.462307", + "version": 0, + "url": "https://ge.xhamster.com/videos/uncontrollable-load-blown-in-stepdaughters-mouth-by-lucky-stepdad-xh8dYFa", + "review": 0, + "should_download": 0, + "title": "Unkontrollierbare ladung wird von gl\u00fccklichem stiefvater in den mund der stieftochter geblasen | xHamster", + "file_name": "Unkontrollierbare ladung wird von gl\u00fccklichem stiefvater in den mund der stieftochter geblasen [xh8dYFa].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/663e820a-b8d1-4711-8dd8-78a1476e4d1d.mp4" + }, + { + "id": "6648bb75-f56f-4f58-a3cb-0dd5746918f1", + "created_date": "2024-07-25 07:29:45.090874", + "last_modified_date": "2024-07-25 07:29:45.090874", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepmom-on-vacation-seduces-stepson-on-the-beach-pov-xhVLQCp", + "review": 0, + "should_download": 0, + "title": "Stiefmutter im Urlaub verf\u00fchrt Stiefsohn am Strand (POV) | xHamster", + "file_name": "Stiefmutter im Urlaub verf\u00fchrt Stiefsohn am Strand (POV) [xhVLQCp].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6648bb75-f56f-4f58-a3cb-0dd5746918f1.mp4" + }, + { + "id": "6678b33d-eda5-49fb-a2a1-c9ac9ab034fb", + "created_date": "2024-07-25 07:29:47.213746", + "last_modified_date": "2024-07-25 07:29:47.213746", + "version": 0, + "url": "https://ge.xhamster.com/videos/sex-office-part-6-four-big-dicks-for-employee-of-the-month-xhdfCVT", + "review": 0, + "should_download": 0, + "title": "Sexb\u00fcro, teil 6. Vier gro\u00dfe schw\u00e4nze f\u00fcr angestellte des monats | xHamster", + "file_name": "Sexb\u00fcro, teil 6. Vier gro\u00dfe schw\u00e4nze f\u00fcr angestellte des monats [xhdfCVT].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6678b33d-eda5-49fb-a2a1-c9ac9ab034fb.mp4" + }, + { + "id": "66ab05fa-543c-4097-8c10-eb55eefd952f", + "created_date": "2024-07-25 07:29:46.587412", + "last_modified_date": "2024-07-25 07:29:46.587412", + "version": 0, + "url": "https://ge.xhamster.com/videos/you-earned-it-8148174", + "review": 0, + "should_download": 0, + "title": "Du hast es verdient | xHamster", + "file_name": "Du hast es verdient [8148174].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/66ab05fa-543c-4097-8c10-eb55eefd952f.mp4" + }, + { + "id": "66b0aaf0-1df1-4831-ae68-e851f8c57157", + "created_date": "2024-07-25 07:29:46.971179", + "last_modified_date": "2024-07-25 07:29:46.971179", + "version": 0, + "url": "https://ge.xhamster.com/videos/der-saft-muss-raus-1976-10203335", + "review": 0, + "should_download": 0, + "title": "Der Saft Muss Raus 1976, Free European HD Porn ed | xHamster", + "file_name": "Der Saft muss raus ... (1976) [10203335].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/66b0aaf0-1df1-4831-ae68-e851f8c57157.mp4" + }, + { + "id": "673fd24c-4bb1-4cf4-a5cf-baaa60185832", + "created_date": "2024-08-28 23:21:54.345181", + "last_modified_date": "2024-08-28 23:21:54.345181", + "version": 0, + "url": "https://ge.xhamster.com/videos/a-filthy-family-bi-video-10819568", + "review": 0, + "should_download": 0, + "title": "Ein schmutziges Familien-Bi-Video | xHamster", + "file_name": "Ein schmutziges Familien-Bi-Video [10819568].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/673fd24c-4bb1-4cf4-a5cf-baaa60185832.mp4" + }, + { + "id": "67629fc7-8a43-4681-afa3-0857b4db93b6", + "created_date": "2024-07-25 07:29:45.591444", + "last_modified_date": "2024-07-25 07:29:45.591444", + "version": 0, + "url": "https://ge.xhamster.com/videos/tight-teen-gets-her-fist-gangbang-an-stepdads-party-and-her-holes-are-not-virgin-anymore-xhaglVF", + "review": 0, + "should_download": 0, + "title": "Enge Teenie bekommt ihren ersten Gangbang auf der Party ihres Stiefvaters und ihre L\u00f6cher sind nicht mehr jungfr\u00e4ulich! | xHamster", + "file_name": "Enge Teenie bekommt ihren ersten Gangbang auf der Party ihres Stiefvaters und ihre L\u00f6cher sind nicht mehr jungfr\u00e4ulich! [xhaglVF].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/67629fc7-8a43-4681-afa3-0857b4db93b6.mp4" + }, + { + "id": "67bbefe9-272e-4770-b75d-08b76374c203", + "created_date": "2024-07-25 07:29:47.577760", + "last_modified_date": "2024-07-25 07:29:47.577760", + "version": 0, + "url": "https://ge.xhamster.com/videos/sexplosion-ibiza-1988-6208646", + "review": 0, + "should_download": 0, + "title": "Sexplosion Ibiza (1988) | xHamster", + "file_name": "Sexplosion Ibiza (1988) [6208646].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/67bbefe9-272e-4770-b75d-08b76374c203.mp4" + }, + { + "id": "67d59112-e5a3-468e-87eb-1dcd1add6268", + "created_date": "2024-07-25 07:29:47.468563", + "last_modified_date": "2024-07-25 07:29:47.468563", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepmom-if-you-want-pleasure-sit-on-my-dick-xhSB21d", + "review": 0, + "should_download": 0, + "title": "Stiefmutter: Wenn du Lust haben willst, setz dich auf meinen Schwanz | xHamster", + "file_name": "Stiefmutter\uff1a Wenn du Lust haben willst, setz dich auf meinen Schwanz [xhSB21d].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/67d59112-e5a3-468e-87eb-1dcd1add6268.mp4" + }, + { + "id": "686a5095-b3a8-43be-a9e1-390f4d0db810", + "created_date": "2024-07-25 07:29:47.442492", + "last_modified_date": "2024-07-25 07:29:47.442492", + "version": 0, + "url": "https://ge.xhamster.com/videos/i-made-up-my-mind-im-gonna-fuck-your-uncle-xhzbcRn", + "review": 0, + "should_download": 0, + "title": "Ich habe mich entschieden, ich werde deinen Onkel ficken! | xHamster", + "file_name": "Ich habe mich entschieden, ich werde deinen Onkel ficken! [xhzbcRn].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/686a5095-b3a8-43be-a9e1-390f4d0db810.mp4" + }, + { + "id": "6888c0ae-638d-4dbd-a49a-eea3bc844914", + "created_date": "2024-07-25 07:29:46.810116", + "last_modified_date": "2024-07-25 07:29:46.810116", + "version": 0, + "url": "https://ge.xhamster.com/videos/private-jessica-moore-enjoys-a-hot-dp-while-on-a-tropical-beach-xhm2biL", + "review": 0, + "should_download": 0, + "title": "Jessica Moore genie\u00dft eine hei\u00dfe Doppelpenetration an einem tropischen Strand | xHamster", + "file_name": "Jessica Moore genie\u00dft eine hei\u00dfe Doppelpenetration an einem tropischen Strand [xhm2biL].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6888c0ae-638d-4dbd-a49a-eea3bc844914.mp4" + }, + { + "id": "6902b6f8-cc57-457b-a704-ac68ef79c75c", + "created_date": "2024-07-25 07:29:47.650192", + "last_modified_date": "2024-07-25 07:29:47.650192", + "version": 0, + "url": "https://ge.xhamster.com/videos/momsteachsex-perv-milf-has-teen-please-sons-cock-s8-e7-10114594", + "review": 0, + "should_download": 0, + "title": "Momsteachsex - perverse MILF hat Teen bitte den Schwanz ihres Sohnes s8: e7 | xHamster", + "file_name": "Momsteachsex - perverse MILF hat Teen bitte den Schwanz ihres Sohnes s8\uff1a e7 [10114594].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6902b6f8-cc57-457b-a704-ac68ef79c75c.mp4" + }, + { + "id": "6915e5bb-6942-4cb8-9934-9af2a2b8085b", + "created_date": "2024-07-25 07:29:45.359614", + "last_modified_date": "2024-07-25 07:29:45.359614", + "version": 0, + "url": "https://ge.xhamster.com/videos/doppelter-genussclub-full-movie-xh9ag5T", + "review": 0, + "should_download": 0, + "title": "Doppelter Genussclub Full Movie, Free Porn be | xHamster", + "file_name": "Doppelter Genussclub (Full Movie) [xh9ag5T].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/6915e5bb-6942-4cb8-9934-9af2a2b8085b.mp4" + }, + { + "id": "69323afc-d33e-435a-b77b-118486b059cb", + "created_date": "2024-07-25 07:29:46.687851", + "last_modified_date": "2024-07-25 07:29:46.687851", + "version": 0, + "url": "https://ge.xhamster.com/videos/sex-club-holidays-1992-carol-lynn-beatrice-valle-kerry-s-9080782", + "review": 0, + "should_download": 0, + "title": "Sexclub-Ferien (1992) Carol Lynn, Beatrice Valle, Kerry s | xHamster", + "file_name": "Sexclub-Ferien (1992) Carol Lynn, Beatrice Valle, Kerry s [9080782].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/69323afc-d33e-435a-b77b-118486b059cb.mp4" + }, + { + "id": "699d58a2-51eb-4cad-af1f-c91721e652cb", + "created_date": "2024-07-25 07:29:45.411998", + "last_modified_date": "2024-07-25 07:29:45.411998", + "version": 0, + "url": "https://ge.xhamster.com/videos/porno-classics-vol-5-xhMdZ0E", + "review": 0, + "should_download": 0, + "title": "Die verfickten Zwillinge | xHamster", + "file_name": "Die verfickten Zwillinge [xhMdZ0E].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/699d58a2-51eb-4cad-af1f-c91721e652cb.mp4" + }, + { + "id": "6a295aa4-389d-4613-944b-dea59349759a", + "created_date": "2024-07-25 07:29:45.989649", + "last_modified_date": "2024-07-25 07:29:45.989649", + "version": 0, + "url": "https://ge.xhamster.com/videos/digitalplayground-my-best-friends-parents-carolina-sweets-9599204", + "review": 0, + "should_download": 0, + "title": "Digitalplayground - die Eltern meines besten Freundes Carolina Sweets | xHamster", + "file_name": "Digitalplayground - die Eltern meines besten Freundes Carolina Sweets [9599204].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6a295aa4-389d-4613-944b-dea59349759a.mp4" + }, + { + "id": "6a7f1452-ce56-4a36-b13a-2100f6e23d98", + "created_date": "2024-07-25 07:29:46.979361", + "last_modified_date": "2024-07-25 07:29:46.979361", + "version": 0, + "url": "https://ge.xhamster.com/videos/daddy4k-amazing-sex-action-of-older-stepdad-and-two-young-12698288", + "review": 0, + "should_download": 0, + "title": "Daddy4k. Erstaunliche Sex-Action von \u00e4lterem Stiefvater und zwei Jungen | xHamster", + "file_name": "Daddy4k. Erstaunliche Sex-Action von \u00e4lterem Stiefvater und zwei Jungen [12698288].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6a7f1452-ce56-4a36-b13a-2100f6e23d98.mp4" + }, + { + "id": "6b02a638-3caf-4e79-85b9-375c350abf43", + "created_date": "2024-07-25 07:29:46.019161", + "last_modified_date": "2024-07-25 07:29:46.019161", + "version": 0, + "url": "https://ge.xhamster.com/videos/happy-stepmom-gets-a-double-birthday-special-from-her-loving-step-son-his-huge-dick-friend-mylf-xhcRjUB", + "review": 0, + "should_download": 0, + "title": "Gl\u00fcckliche stiefmutter bekommt ein doppeltes geburtstags-special von ihrem liebenden stiefsohn und seiner freundin mit riesigem schwanz - MYLF | xHamster", + "file_name": "Gl\u00fcckliche stiefmutter bekommt ein doppeltes geburtstags-special von ihrem liebenden stiefsohn und seiner freundin mit riesigem schwanz - MYLF [xhcRjUB].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6b02a638-3caf-4e79-85b9-375c350abf43.mp4" + }, + { + "id": "6b1961e2-2038-421b-a6d2-43bc20226b20", + "created_date": "2024-07-25 07:29:48.063150", + "last_modified_date": "2024-07-25 07:29:48.063150", + "version": 0, + "url": "https://ge.xhamster.com/videos/bumsfidele-fick-hotel-das-bums-fidele-fick-hotel-xhbu67u", + "review": 0, + "should_download": 0, + "title": "Bumsfidele Fick-hotel - Das Bums-fidele Fick-hotel: Porn a8 | xHamster", + "file_name": "Bumsfidele Fick-Hotel - Das bums-fidele Fick-Hotel [xhbu67u].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6b1961e2-2038-421b-a6d2-43bc20226b20.mp4" + }, + { + "id": "6b5b8586-e116-455a-90f7-3dfb82715701", + "created_date": "2024-07-25 07:29:44.311214", + "last_modified_date": "2024-07-25 07:29:44.311214", + "version": 0, + "url": "https://ge.xhamster.com/videos/geschwisterliebe-ist-strafbar-9149693", + "review": 0, + "should_download": 0, + "title": "Geschwisterliebe Ist Strafbar, Free German Porn Video a8 | xHamster", + "file_name": "Geschwisterliebe ist Strafbar [9149693].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6b5b8586-e116-455a-90f7-3dfb82715701.mp4" + }, + { + "id": "6b9fc3ed-ee15-4902-9304-e9963234ff41", + "created_date": "2024-07-25 07:29:47.340860", + "last_modified_date": "2024-07-25 07:29:47.340860", + "version": 0, + "url": "https://ge.xhamster.com/videos/outdoor-group-sex-with-horny-vacationers-xhOcml6", + "review": 0, + "should_download": 0, + "title": "Outdoor Gruppensex mit geilen Urlauberinnen | xHamster", + "file_name": "Outdoor Gruppensex mit geilen Urlauberinnen [xhOcml6].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6b9fc3ed-ee15-4902-9304-e9963234ff41.mp4" + }, + { + "id": "6bb28414-2353-4fc4-970f-904236dd4344", + "created_date": "2024-07-25 07:29:44.749467", + "last_modified_date": "2024-07-25 07:29:44.749467", + "version": 0, + "url": "https://ge.xhamster.com/videos/suburban-taboo-xhZqZDG", + "review": 0, + "should_download": 0, + "title": "Vorstadt-Tabu | xHamster", + "file_name": "Vorstadt-Tabu [xhZqZDG].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6bb28414-2353-4fc4-970f-904236dd4344.mp4" + }, + { + "id": "6bd69b1f-c08e-46ab-be7e-f157b9322e2b", + "created_date": "2024-07-25 07:29:47.383071", + "last_modified_date": "2024-07-25 07:29:47.383071", + "version": 0, + "url": "https://ge.xhamster.com/videos/reifeprufung-inder-sex-schule-full-movie-1157605", + "review": 0, + "should_download": 0, + "title": "Reifeprufung inder Sex-Schule, kompletter Film | xHamster", + "file_name": "Reifeprufung inder Sex-Schule, kompletter Film [1157605].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6bd69b1f-c08e-46ab-be7e-f157b9322e2b.mp4" + }, + { + "id": "6be6d612-883f-4ee0-b330-f9b8312e89da", + "created_date": "2024-07-25 07:29:47.156065", + "last_modified_date": "2024-07-25 07:29:47.156065", + "version": 0, + "url": "https://ge.xhamster.com/videos/all-in-the-family-1972-10203342", + "review": 0, + "should_download": 0, + "title": "Alles in der Familie (1972) | xHamster", + "file_name": "Alles in der Familie (1972) [10203342].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6be6d612-883f-4ee0-b330-f9b8312e89da.mp4" + }, + { + "id": "6c44c384-0b6e-4d82-a26e-3d405a39c669", + "created_date": "2024-07-25 07:29:44.543623", + "last_modified_date": "2024-07-25 07:29:44.543623", + "version": 0, + "url": "https://ge.xhamster.com/videos/saggy-tits-secretary-fucked-by-her-boss-and-three-other-employees-xh9yGkT", + "review": 0, + "should_download": 0, + "title": "H\u00e4ngende Sekret\u00e4rin von ihrem Chef und drei anderen Angestellten gefickt | xHamster", + "file_name": "H\u00e4ngende Sekret\u00e4rin von ihrem Chef und drei anderen Angestellten gefickt [xh9yGkT].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6c44c384-0b6e-4d82-a26e-3d405a39c669.mp4" + }, + { + "id": "6c7c3cd7-f1ac-4844-a840-b7747911ea63", + "created_date": "2024-09-11 10:23:29.175970", + "last_modified_date": "2024-10-21 16:29:16.277000", + "version": 1, + "url": "https://ge.xhamster.com/videos/shocked-stepmom-catches-her-stepson-ploughing-a-tight-pink-18-yrs-old-pussy-familystrokes-xhXUCEy", + "review": 0, + "should_download": 0, + "title": "Schockierte stiefmutter erwischt ihren stiefsohn beim pfl\u00fcgt eine enge rosa 18-j\u00e4hrige muschi, familienschl\u00e4ge | xHamster", + "file_name": "Schockierte stiefmutter erwischt ihren stiefsohn beim pfl\u00fcgt eine enge rosa 18-j\u00e4hrige muschi, familienschl\u00e4ge [xhXUCEy].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/6c7c3cd7-f1ac-4844-a840-b7747911ea63.mp4" + }, + { + "id": "6cb4856e-7fae-4cc0-a658-b3deee358a7c", + "created_date": "2024-07-25 07:29:47.020367", + "last_modified_date": "2024-07-25 07:29:47.020367", + "version": 0, + "url": "https://ge.xhamster.com/videos/redhead-and-brunette-teen-whores-have-hardcore-threesome-fuck-with-a-stud-xhwlFfU", + "review": 0, + "should_download": 0, + "title": "Rothaarige und br\u00fcnette Teen-Huren haben Hardcore-Dreier-Fick mit einem Hengst | xHamster", + "file_name": "Rothaarige und br\u00fcnette Teen-Huren haben Hardcore-Dreier-Fick mit einem Hengst [xhwlFfU].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6cb4856e-7fae-4cc0-a658-b3deee358a7c.mp4" + }, + { + "id": "6ccd5934-aa00-4e97-a2c6-9bbdcda970af", + "created_date": "2024-07-25 07:29:45.146437", + "last_modified_date": "2024-07-25 07:29:45.146437", + "version": 0, + "url": "https://ge.xhamster.com/videos/fuck-the-boss-to-save-my-job-shannon-heels-xhHfead", + "review": 0, + "should_download": 0, + "title": "Fick den Boss, um meinen Job zu retten - Shannon Heels | xHamster", + "file_name": "Fick den Boss, um meinen Job zu retten - Shannon Heels [xhHfead].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6ccd5934-aa00-4e97-a2c6-9bbdcda970af.mp4" + }, + { + "id": "6cf56d99-bf23-4ff7-abf1-07a09b6d5970", + "created_date": "2024-07-25 07:29:48.089654", + "last_modified_date": "2024-07-25 07:29:48.089654", + "version": 0, + "url": "https://ge.xhamster.com/videos/three-horny-amateur-guys-doing-one-cute-teen-girl-2368857", + "review": 0, + "should_download": 0, + "title": "Drei geile Amateur-Typen machen ein s\u00fc\u00dfes Teenie-M\u00e4dchen | xHamster", + "file_name": "Drei geile Amateur-Typen machen ein s\u00fc\u00dfes Teenie-M\u00e4dchen [2368857].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6cf56d99-bf23-4ff7-abf1-07a09b6d5970.mp4" + }, + { + "id": "6d52fb0e-eb74-4449-a8cb-162182c5ef43", + "created_date": "2024-10-21 15:08:43.553201", + "last_modified_date": "2024-10-21 16:29:21.662000", + "version": 1, + "url": "https://ge.xhamster.com/videos/sexy-dayana-is-waiting-for-two-guys-8918095", + "review": 0, + "should_download": 0, + "title": "Sexy Dayana wartet auf zwei Typen | xHamster", + "file_name": "Sexy Dayana wartet auf zwei Typen [8918095].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/6d52fb0e-eb74-4449-a8cb-162182c5ef43.mp4" + }, + { + "id": "6e13a93d-bc45-41ed-83d9-48e0042a1bcb", + "created_date": "2024-07-25 07:29:47.473394", + "last_modified_date": "2024-07-25 07:29:47.473394", + "version": 0, + "url": "https://ge.xhamster.com/videos/german-sexy-tales-full-movie-xhuWl0H", + "review": 0, + "should_download": 0, + "title": "Deutsche sexy Geschichten (kompletter Film) | xHamster", + "file_name": "Deutsche sexy Geschichten (kompletter Film) [xhuWl0H].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6e13a93d-bc45-41ed-83d9-48e0042a1bcb.mp4" + }, + { + "id": "6e40fd97-a6e3-41d2-a764-0ce71ebbba0d", + "created_date": "2024-07-25 07:29:47.982661", + "last_modified_date": "2024-07-25 07:29:47.982661", + "version": 0, + "url": "https://ge.xhamster.com/videos/hot-girl-next-door-fucks-her-stepdads-big-dick-while-stepmoms-away-xhX1aiY", + "review": 0, + "should_download": 0, + "title": "Hei\u00dfes m\u00e4dchen von nebenan fickt den gro\u00dfen schwanz ihres stiefvaters, w\u00e4hrend stiefmutter weg ist | xHamster", + "file_name": "Hei\u00dfes m\u00e4dchen von nebenan fickt den gro\u00dfen schwanz ihres stiefvaters, w\u00e4hrend stiefmutter weg ist [xhX1aiY].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6e40fd97-a6e3-41d2-a764-0ce71ebbba0d.mp4" + }, + { + "id": "6e4fe5c8-7c96-4ec7-98c5-1e600372a583", + "created_date": "2024-09-24 08:11:39.004291", + "last_modified_date": "2024-10-21 16:29:29.122000", + "version": 1, + "url": "https://ge.xhamster.com/videos/can-you-please-lick-my-pussy-begs-sweet-sophia-s26-e5-xhplI2PS", + "review": 0, + "should_download": 0, + "title": "\"Kannst du bitte meine Muschi lecken ??\" bittet, s\u00fc\u00dfe Sophia - s26: e5 | xHamster", + "file_name": ""Kannst du bitte meine Muschi lecken \uff1f\uff1f" bittet, s\u00fc\u00dfe Sophia - s26\uff1a e5 [xhplI2PS].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/6e4fe5c8-7c96-4ec7-98c5-1e600372a583.mp4" + }, + { + "id": "6e5ad66c-db2f-4431-a8ab-97df389d4fdd", + "created_date": "2024-07-25 07:29:46.095200", + "last_modified_date": "2024-07-25 07:29:46.095200", + "version": 0, + "url": "https://ge.xhamster.com/videos/summer-fever-sex-full-movie-xhxtYij", + "review": 0, + "should_download": 0, + "title": "Sommerfieber-Sex (kompletter Film) | xHamster", + "file_name": "Sommerfieber-Sex (kompletter Film) [xhxtYij].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6e5ad66c-db2f-4431-a8ab-97df389d4fdd.mp4" + }, + { + "id": "6e81505b-72d6-4825-bc3a-6442cc98c4c9", + "created_date": "2024-07-25 07:29:45.598839", + "last_modified_date": "2024-07-25 07:29:45.598839", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-full-hd-movie-original-uncut-version-xha44sY", + "review": 0, + "should_download": 0, + "title": "Familie - (Full HD-Film - urspr\u00fcngliche ungeschnittene Version) | xHamster", + "file_name": "Familie - (Full HD-Film - urspr\u00fcngliche ungeschnittene Version) [xha44sY].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/6e81505b-72d6-4825-bc3a-6442cc98c4c9.mp4" + }, + { + "id": "6f2f8d99-7805-4062-83ff-12b008fb9355", + "created_date": "2024-11-10 16:53:33.479608", + "last_modified_date": "2024-11-10 16:53:33.479608", + "version": 0, + "url": "https://ge.xhamster.com/videos/french-11521540", + "review": 0, + "should_download": 0, + "title": "Franz\u00f6sisch | xHamster", + "file_name": "Franz\u00f6sisch [11521540].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/6f2f8d99-7805-4062-83ff-12b008fb9355.mp4" + }, + { + "id": "6f3157a0-85ae-490d-8a3c-444e13d1af02", + "created_date": "2024-07-25 07:29:45.047314", + "last_modified_date": "2024-07-25 07:29:45.047314", + "version": 0, + "url": "https://ge.xhamster.com/videos/meine-schwester-das-geile-flittchen-akt1-by-mdj-cook-401586", + "review": 0, + "should_download": 0, + "title": "Meine schwester, das geile flittchen (akt1) von mdj.cook | xHamster", + "file_name": "Meine schwester, das geile flittchen (akt1) von mdj.cook [401586].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6f3157a0-85ae-490d-8a3c-444e13d1af02.mp4" + }, + { + "id": "6f77c412-1e17-44fc-a710-4c27d43d7bea", + "created_date": "2024-07-25 07:29:47.231793", + "last_modified_date": "2024-07-25 07:29:47.231793", + "version": 0, + "url": "https://ge.xhamster.com/videos/gf-says-i-can-still-taste-her-pussy-on-your-dick-xhmRTFp", + "review": 0, + "should_download": 0, + "title": "Freundin sagt: \"Ich kann ihre Muschi immer noch an deinem Schwanz schmecken\" | xHamster", + "file_name": "Freundin sagt\uff1a \uff02Ich kann ihre Muschi immer noch an deinem Schwanz schmecken\uff02 [xhmRTFp].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6f77c412-1e17-44fc-a710-4c27d43d7bea.mp4" + }, + { + "id": "6fbaa365-9263-4054-8ef6-6cc92232b1cd", + "created_date": "2024-07-25 07:29:47.922469", + "last_modified_date": "2024-07-25 07:29:47.922469", + "version": 0, + "url": "https://ge.xhamster.com/videos/creampie-she-gets-fucked-by-two-men-on-the-beach-3256398", + "review": 0, + "should_download": 0, + "title": "Creampie - sie wird von zwei M\u00e4nnern am Strand gefickt | xHamster", + "file_name": "Creampie - sie wird von zwei M\u00e4nnern am Strand gefickt [3256398].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6fbaa365-9263-4054-8ef6-6cc92232b1cd.mp4" + }, + { + "id": "6fc6c764-6802-4378-946e-6d01222f79e3", + "created_date": "2024-07-25 07:29:47.711521", + "last_modified_date": "2024-07-25 07:29:47.711521", + "version": 0, + "url": "https://ge.xhamster.com/videos/omg-german-family-fucks-with-brothers-stepdaughter-dirty-threesome-xhqAn06", + "review": 0, + "should_download": 0, + "title": "omg deutsche familie fickt mit Stieftochter des Bruders versauter dreckiger Dreier | xHamster", + "file_name": "omg deutsche familie fickt mit Stieftochter des Bruders versauter dreckiger Dreier [xhqAn06].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6fc6c764-6802-4378-946e-6d01222f79e3.mp4" + }, + { + "id": "6fcdfd01-fb65-4b45-ad9e-d12cc96f212a", + "created_date": "2024-07-25 07:29:47.330129", + "last_modified_date": "2024-07-25 07:29:47.330129", + "version": 0, + "url": "https://ge.xhamster.com/videos/step-nephew-fucks-his-stepaunt-again-family-fantacy-xhjZUAd", + "review": 0, + "should_download": 0, + "title": "Stiefneffe fickt wieder seine stieftante - familienfantasie | xHamster", + "file_name": "Stiefneffe fickt wieder seine stieftante - familienfantasie [xhjZUAd].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6fcdfd01-fb65-4b45-ad9e-d12cc96f212a.mp4" + }, + { + "id": "6fdf94e9-72f0-4b6c-b853-e57de60395e7", + "created_date": "2024-07-25 07:29:47.160008", + "last_modified_date": "2024-07-25 07:29:47.160008", + "version": 0, + "url": "https://ge.xhamster.com/videos/first-orgy-for-his-wife-xhkYZNe", + "review": 0, + "should_download": 0, + "title": "Erste Orgie f\u00fcr seine Ehefrau | xHamster", + "file_name": "Erste Orgie f\u00fcr seine Ehefrau [xhkYZNe].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/6fdf94e9-72f0-4b6c-b853-e57de60395e7.mp4" + }, + { + "id": "70134685-8910-40f5-b83c-84a2f7973fcd", + "created_date": "2024-08-28 23:21:54.378179", + "last_modified_date": "2024-08-28 23:21:54.378179", + "version": 0, + "url": "https://ge.xhamster.com/videos/a-steamy-afternoon-s18-e12-xhPvwgB", + "review": 0, + "should_download": 0, + "title": "Ein dampfiger nachmittag - s18: E12 | xHamster", + "file_name": "Ein dampfiger nachmittag - s18\uff1a E12 [xhPvwgB].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/70134685-8910-40f5-b83c-84a2f7973fcd.mp4" + }, + { + "id": "70666a85-f470-4b2b-bd5a-6bcb9ba2e7c2", + "created_date": "2024-07-25 07:29:46.049429", + "last_modified_date": "2024-07-25 07:29:46.049429", + "version": 0, + "url": "https://ge.xhamster.com/videos/swap-step-mom-says-im-naked-you-should-get-naked-too-xhfaim2", + "review": 0, + "should_download": 0, + "title": "Swap-Stiefmutter sagt, ich bin nackt, du solltest dich auch nackt machen! | xHamster", + "file_name": "Swap-Stiefmutter sagt, ich bin nackt, du solltest dich auch nackt machen! [xhfaim2].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/70666a85-f470-4b2b-bd5a-6bcb9ba2e7c2.mp4" + }, + { + "id": "708556d1-498b-4ccd-8ebc-9e78c06e9239", + "created_date": "2024-07-25 07:29:46.694986", + "last_modified_date": "2024-07-25 07:29:46.694986", + "version": 0, + "url": "https://ge.xhamster.com/videos/wilde-spiele-full-german-movie-xh18KdA", + "review": 0, + "should_download": 0, + "title": "Wilde Spiele- Full German Movie, Free Porn b3 | xHamster", + "file_name": "Wilde Spiele- full german movie [xh18KdA].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/708556d1-498b-4ccd-8ebc-9e78c06e9239.mp4" + }, + { + "id": "70860f46-ad0a-4ec9-b588-5e7059ff208c", + "created_date": "2024-07-25 07:29:47.348035", + "last_modified_date": "2024-07-25 07:29:47.348035", + "version": 0, + "url": "https://ge.xhamster.com/videos/huge-tits-secretary-caught-monster-cock-boss-fuck-and-join-14923480", + "review": 0, + "should_download": 0, + "title": "Mega Titten Sekret\u00e4rin erwischt Boss beim ficken und macht einfach mit | xHamster", + "file_name": "Mega Titten Sekret\u00e4rin erwischt Boss beim ficken und macht einfach mit [14923480].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/70860f46-ad0a-4ec9-b588-5e7059ff208c.mp4" + }, + { + "id": "709ef7de-4f5c-4e0c-82e7-be1c2e459e5d", + "created_date": "2024-07-25 07:29:47.639533", + "last_modified_date": "2024-07-25 07:29:47.639533", + "version": 0, + "url": "https://ge.xhamster.com/videos/dirty-diary-full-movie-xh4vFyh", + "review": 0, + "should_download": 0, + "title": "Schmutziges Tagebuch (kompletter Film) | xHamster", + "file_name": "Schmutziges Tagebuch (kompletter Film) [xh4vFyh].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/709ef7de-4f5c-4e0c-82e7-be1c2e459e5d.mp4" + }, + { + "id": "70aeb534-f28b-4ebc-85e5-13695d794dff", + "created_date": "2024-08-28 23:21:54.361625", + "last_modified_date": "2024-08-28 23:21:54.361625", + "version": 0, + "url": "https://ge.xhamster.com/videos/mofos-house-party-turns-into-orgy-4036263", + "review": 0, + "should_download": 0, + "title": "Mofos - Hausparty wird zur Orgie | xHamster", + "file_name": "Mofos - Hausparty wird zur Orgie [4036263].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/70aeb534-f28b-4ebc-85e5-13695d794dff.mp4" + }, + { + "id": "70b1c610-9a34-466c-88c7-43c97533183a", + "created_date": "2024-07-25 07:29:46.252903", + "last_modified_date": "2024-07-25 07:29:46.252903", + "version": 0, + "url": "https://ge.xhamster.com/videos/blond-secretary-fucking-with-boss-to-keep-her-job-xhw6hoD", + "review": 0, + "should_download": 0, + "title": "Blonde Sekret\u00e4rin fickt mit Chef, um ihren Job zu behalten | xHamster", + "file_name": "Blonde Sekret\u00e4rin fickt mit Chef, um ihren Job zu behalten [xhw6hoD].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/70b1c610-9a34-466c-88c7-43c97533183a.mp4" + }, + { + "id": "70b88562-f3d7-4666-b1bb-43556b6988eb", + "created_date": "2024-07-25 07:29:45.514489", + "last_modified_date": "2024-07-25 07:29:45.514489", + "version": 0, + "url": "https://ge.xhamster.com/videos/caligola-1979-xh0MCXj", + "review": 0, + "should_download": 0, + "title": "Caligola (1979) | xHamster", + "file_name": "Caligola (1979) [xh0MCXj].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/70b88562-f3d7-4666-b1bb-43556b6988eb.mp4" + }, + { + "id": "70fd70de-ff9f-400c-9d04-30e24255eb81", + "created_date": "2024-07-25 07:29:45.895911", + "last_modified_date": "2024-07-25 07:29:45.895911", + "version": 0, + "url": "https://ge.xhamster.com/videos/hemmungsloser-sommerfick-ganzer-film-xhfgd1t", + "review": 0, + "should_download": 0, + "title": "Hemmungsloser Sommerfick Ganzer Film, HD Porn 37 | xHamster", + "file_name": "Hemmungsloser Sommerfick (GANZER FILM) [xhfgd1t].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/70fd70de-ff9f-400c-9d04-30e24255eb81.mp4" + }, + { + "id": "7128f33b-c370-4a30-a3fd-d68e6d925d52", + "created_date": "2025-01-16 19:59:30.841299", + "last_modified_date": "2025-01-16 19:59:30.841305", + "version": 0, + "url": "https://ge.xhamster.com/videos/group-sex-parties-3799842", + "review": 0, + "should_download": 0, + "title": "Gruppensex-Partys | xHamster", + "file_name": "Gruppensex-Partys [3799842].mp4", + "path": null, + "cloud_link": "/data/media/7128f33b-c370-4a30-a3fd-d68e6d925d52.mp4" + }, + { + "id": "713d1c70-d49e-4636-aff6-dfe55451616e", + "created_date": "2024-07-25 07:29:46.011939", + "last_modified_date": "2024-07-25 07:29:46.011939", + "version": 0, + "url": "https://ge.xhamster.com/videos/almost-virgin-two-teens-make-their-first-exchange-xhYnVxR", + "review": 0, + "should_download": 0, + "title": "Fast jungfr\u00e4ulich: Zwei Teenager machen ihren ersten Austausch! | xHamster", + "file_name": "Fast jungfr\u00e4ulich\uff1a Zwei Teenager machen ihren ersten Austausch! [xhYnVxR].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/713d1c70-d49e-4636-aff6-dfe55451616e.mp4" + }, + { + "id": "71874c7f-7c79-4b6f-942e-fa8105f94b5f", + "created_date": "2024-07-25 07:29:44.358918", + "last_modified_date": "2024-07-25 07:29:44.358918", + "version": 0, + "url": "https://ge.xhamster.com/videos/if-you-want-the-job-then-get-naked-fm14-131460", + "review": 0, + "should_download": 0, + "title": "Wenn Sie den Job wollen, dann nackt fm14 | xHamster", + "file_name": "Wenn Sie den Job wollen, dann nackt fm14 [131460].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/71874c7f-7c79-4b6f-942e-fa8105f94b5f.mp4" + }, + { + "id": "71a29116-ec7f-4929-b362-4fbb9262a72f", + "created_date": "2024-12-29 23:53:27.914657", + "last_modified_date": "2024-12-29 23:53:27.914657", + "version": 0, + "url": "https://ge.xhamster.com/videos/mommys-friend-is-as-slutty-as-her-12005465", + "review": 0, + "should_download": 0, + "title": "Mommys Freundin ist so versaut wie sie | xHamster", + "file_name": "Mommys Freundin ist so versaut wie sie [12005465].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/71a29116-ec7f-4929-b362-4fbb9262a72f.mp4" + }, + { + "id": "71c69654-9bfc-46b4-b5ee-1bca39a4e2b6", + "created_date": "2024-07-25 07:29:46.141002", + "last_modified_date": "2024-07-25 07:29:46.141002", + "version": 0, + "url": "https://ge.xhamster.com/videos/teenies-der-schule-full-movie-xhS1WDp", + "review": 0, + "should_download": 0, + "title": "Teenies Der Schule Full Movie, Free Big Cock HD Porn 22 | xHamster", + "file_name": "Teenies der Schule (Full Movie) [xhS1WDp].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/71c69654-9bfc-46b4-b5ee-1bca39a4e2b6.mp4" + }, + { + "id": "72465dbd-3013-4e3e-a2a1-266d1ec8eadd", + "created_date": "2024-07-25 07:29:46.949884", + "last_modified_date": "2024-07-25 07:29:46.949884", + "version": 0, + "url": "https://ge.xhamster.com/videos/stp4-lovely-daughter-fucks-two-of-her-step-dads-friends-7340165", + "review": 0, + "should_download": 0, + "title": "Stp4, sch\u00f6ne Tochter fickt zwei ihrer Stiefvater-Freunde! | xHamster", + "file_name": "Stp4, sch\u00f6ne Tochter fickt zwei ihrer Stiefvater-Freunde! [7340165].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/72465dbd-3013-4e3e-a2a1-266d1ec8eadd.mp4" + }, + { + "id": "7274c824-aa20-45e2-8e6f-85793f0856b7", + "created_date": "2024-10-07 20:47:56.415936", + "last_modified_date": "2024-10-21 16:29:37.747000", + "version": 2, + "url": "https://ge.xhamster.com/videos/naked-lunch-vintage-danish-anal-dp-fun-12801397", + "review": 0, + "should_download": 0, + "title": "Nacktes mittagessen: alter d\u00e4nischer anal-doppelpenetrationsspa\u00df | xHamster", + "file_name": "Nacktes mittagessen\uff1a alter d\u00e4nischer anal-doppelpenetrationsspa\u00df [12801397].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/7274c824-aa20-45e2-8e6f-85793f0856b7.mp4" + }, + { + "id": "72a5a42c-dcf8-40db-9e0b-ac65457c4c07", + "created_date": "2024-07-25 07:29:45.670114", + "last_modified_date": "2024-07-25 07:29:45.670114", + "version": 0, + "url": "https://ge.xhamster.com/videos/coed-sucks-dick-and-fucks-on-couch-xhZi1TM", + "review": 0, + "should_download": 0, + "title": "Coed lutscht Schwanz und fickt auf der Couch | xHamster", + "file_name": "Coed lutscht Schwanz und fickt auf der Couch [xhZi1TM].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/72a5a42c-dcf8-40db-9e0b-ac65457c4c07.mp4" + }, + { + "id": "734d7120-ba7d-4ccc-bc4b-c8042602071d", + "created_date": "2025-01-16 19:59:40.815410", + "last_modified_date": "2025-01-16 19:59:40.815416", + "version": 0, + "url": "https://ge.xhamster.com/videos/i-like-feeling-my-stepbrother-inside-me-xhsHVNu", + "review": 0, + "should_download": 0, + "title": "Ich mag es, meinen stiefbrud in mir zu f\u00fchlen | xHamster", + "file_name": "Ich mag es, meinen stiefbrud in mir zu f\u00fchlen [xhsHVNu].mp4", + "path": null, + "cloud_link": "/data/media/734d7120-ba7d-4ccc-bc4b-c8042602071d.mp4" + }, + { + "id": "7350506a-bde2-457a-993d-f513968cfd16", + "created_date": "2024-11-10 16:53:33.482203", + "last_modified_date": "2024-11-10 16:53:33.482203", + "version": 0, + "url": "https://ge.xhamster.com/videos/summer-wind-1-10518825", + "review": 0, + "should_download": 0, + "title": "Sommerwind 1 | xHamster", + "file_name": "Sommerwind 1 [10518825].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/7350506a-bde2-457a-993d-f513968cfd16.mp4" + }, + { + "id": "73a1bd4e-fa43-4ec9-ba3f-e9a976010f55", + "created_date": "2024-07-25 07:29:45.919244", + "last_modified_date": "2024-07-25 07:29:45.919244", + "version": 0, + "url": "https://ge.xhamster.com/videos/la-locandiera-parte-03-xhzkmFZ", + "review": 0, + "should_download": 0, + "title": "La Locandiera - Parte 03, Free Big Cock Porn cb | xHamster", + "file_name": "LA LOCANDIERA - Parte #03 [xhzkmFZ].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/73a1bd4e-fa43-4ec9-ba3f-e9a976010f55.mp4" + }, + { + "id": "73e38a46-c10c-4c22-9cdd-c54a88175df3", + "created_date": "2024-10-07 20:47:56.421616", + "last_modified_date": "2024-10-21 16:29:42.206000", + "version": 1, + "url": "https://ge.xhamster.com/videos/lost-bet-i-had-to-hold-out-my-ass-xhuECyX", + "review": 0, + "should_download": 0, + "title": "Wette verloren! Ich musste meinen Arsch hinhalten! | xHamster", + "file_name": "Wette verloren! Ich musste meinen Arsch hinhalten! [xhuECyX].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/73e38a46-c10c-4c22-9cdd-c54a88175df3.mp4" + }, + { + "id": "73f79729-410b-4aa5-9c6d-4825f3d4028f", + "created_date": "2024-07-25 07:29:45.017905", + "last_modified_date": "2024-07-25 07:29:45.017905", + "version": 0, + "url": "https://ge.xhamster.com/videos/forte-immerscharf6-6611934", + "review": 0, + "should_download": 0, + "title": "Forte Immerscharf6: Free Mature Porn Video e0 | xHamster", + "file_name": "FORTE Immerscharf6 [6611934].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/73f79729-410b-4aa5-9c6d-4825f3d4028f.mp4" + }, + { + "id": "741b0ae6-18c9-49e4-804c-7933c3393ef0", + "created_date": "2024-07-25 07:29:45.677150", + "last_modified_date": "2024-07-25 07:29:45.677150", + "version": 0, + "url": "https://ge.xhamster.com/videos/das-buch-der-sunde-xhwo6ny", + "review": 0, + "should_download": 0, + "title": "Das Buch Der Sunde: European HD Porn Video 8d | xHamster", + "file_name": "Das Buch Der Sunde [xhwo6ny].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/741b0ae6-18c9-49e4-804c-7933c3393ef0.mp4" + }, + { + "id": "742d81c2-1d3a-4dd2-9fca-67e94699552d", + "created_date": "2024-12-29 23:53:27.930890", + "last_modified_date": "2024-12-29 23:53:27.930890", + "version": 0, + "url": "https://ge.xhamster.com/videos/step-sisters-friend-likes-to-watch-s13-e6-xh8FhOx", + "review": 0, + "should_download": 0, + "title": "Die freundin der stiefschwester mag zuschauen - S13: e6 | xHamster", + "file_name": "Die freundin der stiefschwester mag zuschauen - S13\uff1a e6 [xh8FhOx].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/742d81c2-1d3a-4dd2-9fca-67e94699552d.mp4" + }, + { + "id": "745cc566-5e2e-4673-99a5-b5c0518711e4", + "created_date": "2024-07-25 07:29:44.958155", + "last_modified_date": "2024-07-25 07:29:44.958155", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-stepson-helps-me-cheat-and-get-back-at-my-husband-xhDB23m", + "review": 0, + "should_download": 0, + "title": "Mein Stiefsohn hilft mir, meinen Mann zu betr\u00fcgen und es ihm heimzuzahlen | xHamster", + "file_name": "Mein Stiefsohn hilft mir, meinen Mann zu betr\u00fcgen und es ihm heimzuzahlen [xhDB23m].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/745cc566-5e2e-4673-99a5-b5c0518711e4.mp4" + }, + { + "id": "74f94310-2be1-4977-bcc4-107f2634d56c", + "created_date": "2024-09-24 08:11:39.008109", + "last_modified_date": "2024-10-21 16:29:51.586000", + "version": 1, + "url": "https://ge.xhamster.com/videos/mommys-boy-horny-milf-penny-barber-is-caught-by-stepniece-chloe-surreal-while-fucking-her-stepson-xhlW34j", + "review": 0, + "should_download": 0, + "title": "Mommy's boy - die geile milf penny friseur wird von stiefnichte chloe surreal beim ficken ihres stiefsohns erwischt | xHamster", + "file_name": "Mommy's boy - die geile milf penny friseur wird von stiefnichte chloe surreal beim ficken ihres stiefsohns erwischt [xhlW34j].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/74f94310-2be1-4977-bcc4-107f2634d56c.mp4" + }, + { + "id": "750de205-7e84-4b61-adc9-d88ab6ea298a", + "created_date": "2024-07-25 07:29:46.528530", + "last_modified_date": "2024-07-25 07:29:46.528530", + "version": 0, + "url": "https://ge.xhamster.com/videos/i-need-a-dick-in-order-to-fuck-my-friend-can-you-help-adria-rae-begs-stepbro-s9-e7-xhXHcZr", + "review": 0, + "should_download": 0, + "title": "\"Ich brauche einen schwanz, um meinen freund zu ficken, kannst du helfen?\" Adria rae bettelt stiefbrud - s9: e7 | xHamster", + "file_name": "\uff02Ich brauche einen schwanz, um meinen freund zu ficken, kannst du helfen\uff1f\uff02 Adria rae bettelt stiefbrud - s9\uff1a e7 [xhXHcZr].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/750de205-7e84-4b61-adc9-d88ab6ea298a.mp4" + }, + { + "id": "75e166bb-f986-4177-80a0-7de3b06cf5f8", + "created_date": "2024-07-25 07:29:44.458838", + "last_modified_date": "2024-07-25 07:29:44.458838", + "version": 0, + "url": "https://ge.xhamster.com/videos/country-ficks-full-movie-xhSJAjH", + "review": 0, + "should_download": 0, + "title": "Land fickt (kompletter Film) | xHamster", + "file_name": "Land fickt (kompletter Film) [xhSJAjH].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/75e166bb-f986-4177-80a0-7de3b06cf5f8.mp4" + }, + { + "id": "760df509-51a6-452d-8f06-99dd27e91a22", + "created_date": "2024-12-29 23:53:27.923567", + "last_modified_date": "2024-12-29 23:53:27.923567", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-mischiefs-part-3-step-mom-and-step-daughter-teaching-bf-the-art-of-pleasure-xhitNGS", + "review": 0, + "should_download": 0, + "title": "Familien-unfug (teil 3): stiefmutter und stieftochter lehren freund die kunst des vergn\u00fcgens | xHamster", + "file_name": "Familien-unfug (teil 3)\uff1a stiefmutter und stieftochter lehren freund die kunst des vergn\u00fcgens [xhitNGS].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/760df509-51a6-452d-8f06-99dd27e91a22.mp4" + }, + { + "id": "7620ed7f-0620-47b3-9d2d-ba0fabe9c17f", + "created_date": "2024-07-25 07:29:44.388554", + "last_modified_date": "2024-07-25 07:29:44.388554", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-traditions-2-12951442", + "review": 0, + "should_download": 0, + "title": "Familientraditionen 2 | xHamster", + "file_name": "Familientraditionen 2 [12951442].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7620ed7f-0620-47b3-9d2d-ba0fabe9c17f.mp4" + }, + { + "id": "76275eea-b839-4065-8e6a-4fd84d73d6f0", + "created_date": "2024-07-25 07:29:45.086382", + "last_modified_date": "2024-07-25 07:29:45.086382", + "version": 0, + "url": "https://ge.xhamster.com/videos/busty-nala-brooks-tells-her-stepbro-my-boobs-are-you-serious-i-have-a-brain-s16-e2-xhgaoCo", + "review": 0, + "should_download": 0, + "title": "Die vollbusige Nala Brooks sagt ihrem Stiefbruder: \"Meine M\u00f6pse?! Meinst du das ernst, ich habe ein Gehirn!\" - s16: e2 | xHamster", + "file_name": "Die vollbusige Nala Brooks sagt ihrem Stiefbruder\uff1a \uff02Meine M\u00f6pse\uff1f! Meinst du das ernst, ich habe ein Gehirn!\uff02 - s16\uff1a e2 [xhgaoCo].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/76275eea-b839-4065-8e6a-4fd84d73d6f0.mp4" + }, + { + "id": "762a2cdc-3ce0-428d-a9db-5bc92a7ec7ea", + "created_date": "2024-07-25 07:29:47.866937", + "last_modified_date": "2024-07-25 07:29:47.866937", + "version": 0, + "url": "https://ge.xhamster.com/videos/18-and-confused-3-xhn7NA6", + "review": 0, + "should_download": 0, + "title": "18 und verwirrt # 3 | xHamster", + "file_name": "18 und verwirrt # 3 [xhn7NA6].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/762a2cdc-3ce0-428d-a9db-5bc92a7ec7ea.mp4" + }, + { + "id": "763272ee-bad7-42cf-84a9-fbfc04971997", + "created_date": "2024-07-25 07:29:45.256260", + "last_modified_date": "2024-07-25 07:29:45.256260", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepmom-says-i-have-a-better-place-for-your-cum-xhpNKGm", + "review": 0, + "should_download": 0, + "title": "Stiefmutter sagt, ich habe einen besseren Platz f\u00fcr dein Sperma! | xHamster", + "file_name": "Stiefmutter sagt, ich habe einen besseren Platz f\u00fcr dein Sperma! [xhpNKGm].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/763272ee-bad7-42cf-84a9-fbfc04971997.mp4" + }, + { + "id": "77113080-c397-459e-8637-b252a272b7bb", + "created_date": "2024-07-25 07:29:46.208773", + "last_modified_date": "2024-07-25 07:29:46.208773", + "version": 0, + "url": "https://ge.xhamster.com/videos/come-play-with-me-2-1980-restored-7566618", + "review": 0, + "should_download": 0, + "title": "Komm, spiel mit mir 2 - 1980 (restauriert) | xHamster", + "file_name": "Komm, spiel mit mir 2 - 1980 (restauriert) [7566618].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/77113080-c397-459e-8637-b252a272b7bb.mp4" + }, + { + "id": "771fea64-dc90-4d5e-b549-ccbbaadf914d", + "created_date": "2024-07-25 07:29:47.113723", + "last_modified_date": "2024-07-25 07:29:47.113723", + "version": 0, + "url": "https://ge.xhamster.com/videos/sb3-stepdaughter-gets-fucked-by-boyfriend-and-stepdad-6050058", + "review": 0, + "should_download": 0, + "title": "Sb3 Stieftochter wird von Freund und Stiefvater gefickt! | xHamster", + "file_name": "Sb3 Stieftochter wird von Freund und Stiefvater gefickt! [6050058].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/771fea64-dc90-4d5e-b549-ccbbaadf914d.mp4" + }, + { + "id": "777f2fc6-76db-4f5d-be24-3ee15f840a3e", + "created_date": "2024-07-25 07:29:47.036236", + "last_modified_date": "2024-07-25 07:29:47.036236", + "version": 0, + "url": "https://ge.xhamster.com/videos/alyx-star-her-bf-head-to-lauren-pixies-beauty-saloon-for-a-threesome-they-will-never-forget-brazzers-xh0Db0M", + "review": 0, + "should_download": 0, + "title": "Alyx spielt mit ihrem Freund in Lauren Pixies Beauty-Salon f\u00fcr einen Dreier, den sie nie vergessen werden - Brazzers | xHamster", + "file_name": "Alyx spielt mit ihrem Freund in Lauren Pixies Beauty-Salon f\u00fcr einen Dreier, den sie nie vergessen werden - Brazzers [xh0Db0M].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/777f2fc6-76db-4f5d-be24-3ee15f840a3e.mp4" + }, + { + "id": "7867291c-bbd1-40bb-9427-d3b44b9aa40a", + "created_date": "2024-07-25 07:29:47.665681", + "last_modified_date": "2024-07-25 07:29:47.665681", + "version": 0, + "url": "https://ge.xhamster.com/videos/atm-dp-babe-3way-double-ass-n-cuntfucked-by-big-cock-fellows-xhkvgFx", + "review": 0, + "should_download": 0, + "title": "ATM, DP, Sch\u00e4tzchen in Dreier, Doppelarsch und Fotze wird von Typen mit gro\u00dfem Schwanz gefickt | xHamster", + "file_name": "ATM, DP, Sch\u00e4tzchen in Dreier, Doppelarsch und Fotze wird von Typen mit gro\u00dfem Schwanz gefickt [xhkvgFx].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7867291c-bbd1-40bb-9427-d3b44b9aa40a.mp4" + }, + { + "id": "787d5400-f765-4b2f-ab42-6ab4e5bab944", + "created_date": "2024-07-25 07:29:45.082938", + "last_modified_date": "2024-07-25 07:29:45.082938", + "version": 0, + "url": "https://ge.xhamster.com/videos/potters-girlfriend-gets-hard-dick-in-face-while-babysitting-xhw7ckY", + "review": 0, + "should_download": 0, + "title": "Potters Freundin bekommt beim Babysitten einen harten Schwanz ins Gesicht | xHamster", + "file_name": "Potters Freundin bekommt beim Babysitten einen harten Schwanz ins Gesicht [xhw7ckY].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/787d5400-f765-4b2f-ab42-6ab4e5bab944.mp4" + }, + { + "id": "789dd021-a760-4457-a856-2d8b557d3db5", + "created_date": "2024-07-25 07:29:46.918873", + "last_modified_date": "2024-07-25 07:29:46.918873", + "version": 0, + "url": "https://ge.xhamster.com/videos/small-tit-blondie-comes-across-three-horny-men-and-she-gives-xhFwh0v", + "review": 0, + "should_download": 0, + "title": "Blondine mit kleinen Titten trifft auf drei geile M\u00e4nner\u2026 | xHamster", + "file_name": "Blondine mit kleinen Titten trifft auf drei geile M\u00e4nner\u2026 [xhFwh0v].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/789dd021-a760-4457-a856-2d8b557d3db5.mp4" + }, + { + "id": "78b9cc10-6a97-4267-bb42-c575def86f43", + "created_date": "2024-07-25 07:29:47.484073", + "last_modified_date": "2024-07-25 07:29:47.484073", + "version": 0, + "url": "https://ge.xhamster.com/videos/drncm-classic-dp-b24-xhUvIS6", + "review": 0, + "should_download": 0, + "title": "Drncm Klassiker, Doppelpenetration B24 | xHamster", + "file_name": "Drncm Klassiker, Doppelpenetration B24 [xhUvIS6].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/78b9cc10-6a97-4267-bb42-c575def86f43.mp4" + }, + { + "id": "7947f803-84cd-4baa-b34e-e5f4883f2505", + "created_date": "2024-12-29 23:53:27.930013", + "last_modified_date": "2024-12-29 23:53:27.930013", + "version": 0, + "url": "https://ge.xhamster.com/videos/runaway-step-niece-gets-treated-like-a-personal-freeuse-sex-slave-by-her-step-auntie-her-husband-xhleYMJ", + "review": 0, + "should_download": 0, + "title": "Runaway stiefnichte wird von ihrer stieftante und ihrem ehemann wie eine pers\u00f6nliche freie sexsklavin behandelt | xHamster", + "file_name": "Runaway stiefnichte wird von ihrer stieftante und ihrem ehemann wie eine pers\u00f6nliche freie sexsklavin behandelt [xhleYMJ].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/7947f803-84cd-4baa-b34e-e5f4883f2505.mp4" + }, + { + "id": "79e8b6bd-46e6-4559-8729-c7e6c7f45b1c", + "created_date": "2024-07-25 07:29:44.677857", + "last_modified_date": "2024-07-25 07:29:44.677857", + "version": 0, + "url": "https://ge.xhamster.com/videos/perfekte-reitschlampen-full-movie-xhiBqHy", + "review": 0, + "should_download": 0, + "title": "Perfekte Reitschlampen Full Movie, Free Porn da | xHamster", + "file_name": "Perfekte Reitschlampen (Full Movie) [xhiBqHy].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/79e8b6bd-46e6-4559-8729-c7e6c7f45b1c.mp4" + }, + { + "id": "79ec65b5-aaf3-4cc2-8ad0-1df51d326641", + "created_date": "2024-07-25 07:29:48.055937", + "last_modified_date": "2024-07-25 07:29:48.055937", + "version": 0, + "url": "https://ge.xhamster.com/videos/sexy-cute-stepsister-loves-petting-from-me-karolinaorgasm-xhgTZ62", + "review": 0, + "should_download": 0, + "title": "Sexy s\u00fc\u00dfe Stiefschwester liebt es, von mir zu streicheln - Karolinaorgasm | xHamster", + "file_name": "Sexy s\u00fc\u00dfe Stiefschwester liebt es, von mir zu streicheln - Karolinaorgasm [xhgTZ62].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/79ec65b5-aaf3-4cc2-8ad0-1df51d326641.mp4" + }, + { + "id": "79ee0fc8-5058-484c-86fa-5b12fb2557ae", + "created_date": "2024-07-25 07:29:44.392061", + "last_modified_date": "2024-07-25 07:29:44.392061", + "version": 0, + "url": "https://ge.xhamster.com/videos/i-heat-up-voyeurs-at-the-beach-and-end-up-full-of-cum-xhUGRI1", + "review": 0, + "should_download": 0, + "title": "Ich w\u00e4rme Voyeure am Strand auf und lande voller Sperma, | xHamster", + "file_name": "Ich w\u00e4rme Voyeure am Strand auf und lande voller Sperma, [xhUGRI1].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/79ee0fc8-5058-484c-86fa-5b12fb2557ae.mp4" + }, + { + "id": "79febbb5-ae59-4898-a368-1866359a0b8d", + "created_date": "2024-12-29 23:53:27.913574", + "last_modified_date": "2024-12-29 23:53:27.913574", + "version": 0, + "url": "https://ge.xhamster.com/videos/just-another-taboo-orgy-at-the-house-11102874", + "review": 0, + "should_download": 0, + "title": "Nur eine weitere Tabu-Orgie im Haus | xHamster", + "file_name": "Nur eine weitere Tabu-Orgie im Haus [11102874].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/79febbb5-ae59-4898-a368-1866359a0b8d.mp4" + }, + { + "id": "7a4258fb-08d4-47a5-80ee-60e5083839ca", + "created_date": "2024-08-28 23:21:54.371401", + "last_modified_date": "2024-08-28 23:21:54.371401", + "version": 0, + "url": "https://ge.xhamster.com/videos/bffs-summer-camp-counselors-record-lesbian-orgy-4972921", + "review": 0, + "should_download": 0, + "title": "Bffs - Sommercamp-Beraterinnen nehmen lesbische Orgien auf | xHamster", + "file_name": "Bffs - Sommercamp-Beraterinnen nehmen lesbische Orgien auf [4972921].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/7a4258fb-08d4-47a5-80ee-60e5083839ca.mp4" + }, + { + "id": "7a82fafa-4d5e-456c-b7aa-815c8d850d7a", + "created_date": "2024-07-25 07:29:44.861437", + "last_modified_date": "2024-07-25 07:29:44.861437", + "version": 0, + "url": "https://ge.xhamster.com/videos/private-private-com-hot-ellen-betsy-fucks-her-way-to-cheap-house-xhcsO0j", + "review": 0, + "should_download": 0, + "title": "Private.com - die hei\u00dfe Ellen Betsy fickt sich zum billigen Haus! | xHamster", + "file_name": "Private.com - die hei\u00dfe Ellen Betsy fickt sich zum billigen Haus! [xhcsO0j].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/7a82fafa-4d5e-456c-b7aa-815c8d850d7a.mp4" + }, + { + "id": "7a8c8d8c-3fd4-4582-b661-9221e6f157c4", + "created_date": "2024-07-25 07:29:45.639991", + "last_modified_date": "2024-07-25 07:29:45.639991", + "version": 0, + "url": "https://ge.xhamster.com/videos/die-wette-2007-german-tyra-misoux-dvd-xhGNmvY", + "review": 0, + "should_download": 0, + "title": "Die Wette 2007 German Tyra Misoux Dvd, HD Porn 67 | xHamster", + "file_name": "Die Wette (2007, German, Tyra Misoux, DVD) [xhGNmvY].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7a8c8d8c-3fd4-4582-b661-9221e6f157c4.mp4" + }, + { + "id": "7a9c6680-d48f-47fa-b0e9-f71f974321eb", + "created_date": "2024-10-07 20:47:56.416898", + "last_modified_date": "2024-10-21 16:29:59.363000", + "version": 1, + "url": "https://ge.xhamster.com/videos/first-threesome-anal-sex-and-dp-for-lilith-xhJi0oC", + "review": 0, + "should_download": 0, + "title": "Erster dreier, analsex und doppelpenetration f\u00fcr lilith | xHamster", + "file_name": "Erster dreier, analsex und doppelpenetration f\u00fcr lilith [xhJi0oC].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/7a9c6680-d48f-47fa-b0e9-f71f974321eb.mp4" + }, + { + "id": "7aec1ac9-caf7-40c7-846d-707196ec4c8b", + "created_date": "2024-07-25 07:29:47.502456", + "last_modified_date": "2024-07-25 07:29:47.502456", + "version": 0, + "url": "https://ge.xhamster.com/videos/interracial-hippie-orgies-1976-12159586", + "review": 0, + "should_download": 0, + "title": "Interracial Hippie-Orgien (1976) | xHamster", + "file_name": "Interracial Hippie-Orgien (1976) [12159586].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7aec1ac9-caf7-40c7-846d-707196ec4c8b.mp4" + }, + { + "id": "7b098a86-2e68-4742-976f-d6dc17eae2d7", + "created_date": "2024-07-25 07:29:46.881634", + "last_modified_date": "2024-07-25 07:29:46.881634", + "version": 0, + "url": "https://ge.xhamster.com/videos/business-trip-sex-with-naughty-secretary-business-bitch-xhKXDQi", + "review": 0, + "should_download": 0, + "title": "Gesch\u00e4ftsreisen-Sex mit frechen Sekret\u00e4rin, Business-Schlampe | xHamster", + "file_name": "Gesch\u00e4ftsreisen-Sex mit frechen Sekret\u00e4rin, Business-Schlampe [xhKXDQi].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7b098a86-2e68-4742-976f-d6dc17eae2d7.mp4" + }, + { + "id": "7b14c2d0-4913-425f-af47-1f04ba6ff0b3", + "created_date": "2024-07-25 07:29:48.176365", + "last_modified_date": "2024-07-25 07:29:48.176365", + "version": 0, + "url": "https://ge.xhamster.com/videos/passion-hd-motivated-assistant-fucks-her-boss-for-raise-xhNeGygY", + "review": 0, + "should_download": 0, + "title": "Passion-hd motivierte Assistentin fickt ihren Chef f\u00fcr Gehaltserh\u00f6hung | xHamster", + "file_name": "Passion-hd motivierte Assistentin fickt ihren Chef f\u00fcr Gehaltserh\u00f6hung [xhNeGygY].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7b14c2d0-4913-425f-af47-1f04ba6ff0b3.mp4" + }, + { + "id": "7b87eace-162b-4ebc-8080-616fd20dd1bf", + "created_date": "2024-07-25 07:29:48.151451", + "last_modified_date": "2024-07-25 07:29:48.151451", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-fun-921359", + "review": 0, + "should_download": 0, + "title": "Familienspa\u00df | xHamster", + "file_name": "Familienspa\u00df [921359].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7b87eace-162b-4ebc-8080-616fd20dd1bf.mp4" + }, + { + "id": "7bde1b95-498b-4a90-a079-990d5ec0e023", + "created_date": "2025-01-17 16:34:37.854825", + "last_modified_date": "2025-01-17 16:34:37.854831", + "version": 0, + "url": "https://ge.xhamster.com/videos/how-to-play-the-game-s40-e4-xhKgE88", + "review": 0, + "should_download": 0, + "title": "Wie man das Spiel spielt - s40: e4 | xHamster", + "file_name": "Wie man das Spiel spielt - s40\uff1a e4 [xhKgE88].mp4", + "path": null, + "cloud_link": "/data/media/7bde1b95-498b-4a90-a079-990d5ec0e023.mp4" + }, + { + "id": "7bea39b5-5324-4640-9f5a-a339ce321110", + "created_date": "2024-07-25 07:29:47.725840", + "last_modified_date": "2024-07-25 07:29:47.725840", + "version": 0, + "url": "https://ge.xhamster.com/videos/whitney-aj-alexis-and-tony-unleash-their-sexual-tensions-after-the-game-xh9QfP3", + "review": 0, + "should_download": 0, + "title": "Whitney, aj, Alexis und Tony l\u00f6sen ihre sexuellen Spannungen nach dem Spiel | xHamster", + "file_name": "Whitney, aj, Alexis und Tony l\u00f6sen ihre sexuellen Spannungen nach dem Spiel [xh9QfP3].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7bea39b5-5324-4640-9f5a-a339ce321110.mp4" + }, + { + "id": "7c0a109e-f581-454f-b05c-c793784484d8", + "created_date": "2024-07-25 07:29:46.503687", + "last_modified_date": "2024-07-25 07:29:46.503687", + "version": 0, + "url": "https://ge.xhamster.com/videos/fun-and-games-with-step-sis-xhDT2Uy", + "review": 0, + "should_download": 0, + "title": "Spa\u00df und Spiele mit Stiefschwester | xHamster", + "file_name": "Spa\u00df und Spiele mit Stiefschwester [xhDT2Uy].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7c0a109e-f581-454f-b05c-c793784484d8.mp4" + }, + { + "id": "7c2f7c8c-dda5-4371-9a9c-657392ef3c5d", + "created_date": "2024-07-25 07:29:46.111905", + "last_modified_date": "2024-07-25 07:29:46.111905", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsis-says-i-just-want-to-see-what-kind-of-porn-you-watch-xhbALMG", + "review": 0, + "should_download": 0, + "title": "Stiefschwester sagt, ich will nur sehen, welche Art von Porno du dir ansiehst! | xHamster", + "file_name": "Stiefschwester sagt, ich will nur sehen, welche Art von Porno du dir ansiehst! [xhbALMG].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7c2f7c8c-dda5-4371-9a9c-657392ef3c5d.mp4" + }, + { + "id": "7cc35476-a522-4b5e-a22c-523b32140d50", + "created_date": "2024-07-25 07:29:48.105231", + "last_modified_date": "2024-07-25 07:29:48.105231", + "version": 0, + "url": "https://ge.xhamster.com/videos/sex-geschichten-heisser-braute-full-hd-movie-original-xhnymJw", + "review": 0, + "should_download": 0, + "title": "Sex Geschichten Heisser Braute - Full HD Movie - Original | xHamster", + "file_name": "SEX GESCHICHTEN HEISSER BRAUTE - (Full HD Movie - Original [xhnymJw].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7cc35476-a522-4b5e-a22c-523b32140d50.mp4" + }, + { + "id": "7cdf1a74-b863-4af8-9f92-aafda41ccd0c", + "created_date": "2024-07-25 07:29:47.261523", + "last_modified_date": "2024-07-25 07:29:47.261523", + "version": 0, + "url": "https://ge.xhamster.com/videos/3-way-porn-big-boat-group-sex-party-part-2-13327664", + "review": 0, + "should_download": 0, + "title": "3-Wege-Porno - Gruppensex-Party mit gro\u00dfem Boot - Teil 2 | xHamster", + "file_name": "3-Wege-Porno - Gruppensex-Party mit gro\u00dfem Boot - Teil 2 [13327664].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7cdf1a74-b863-4af8-9f92-aafda41ccd0c.mp4" + }, + { + "id": "7d20b1c1-98e4-4eae-a524-68d692485170", + "created_date": "2024-07-25 07:29:47.284085", + "last_modified_date": "2024-07-25 07:29:47.284085", + "version": 0, + "url": "https://ge.xhamster.com/videos/nudist-darts-squirt-my-plan-didnt-work-out-over-bullseye-in-my-pussy-xhw0vJE", + "review": 0, + "should_download": 0, + "title": "FKK DARTS SQUIRT ORGIE! Mein Plan ging nicht auf! \u00dcbers Bullseye in meine Muschi! | xHamster", + "file_name": "FKK DARTS SQUIRT ORGIE! Mein Plan ging nicht auf! \u00dcbers Bullseye in meine Muschi! [xhw0vJE].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7d20b1c1-98e4-4eae-a524-68d692485170.mp4" + }, + { + "id": "7d213f63-0333-4d5a-a9db-35fa31573fde", + "created_date": "2024-07-25 07:29:45.865717", + "last_modified_date": "2024-07-25 07:29:45.865717", + "version": 0, + "url": "https://ge.xhamster.com/videos/daily-stories-of-young-sluts-full-movie-xhOVI3v", + "review": 0, + "should_download": 0, + "title": "T\u00e4gliche geschichten von jungen schlampen - KOMPLETTER FILM | xHamster", + "file_name": "T\u00e4gliche geschichten von jungen schlampen - KOMPLETTER FILM [xhOVI3v].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7d213f63-0333-4d5a-a9db-35fa31573fde.mp4" + }, + { + "id": "7d65d3d8-ddf6-44b4-887d-bfdc6d7b4256", + "created_date": "2024-07-25 07:29:46.679494", + "last_modified_date": "2024-07-25 07:29:46.679494", + "version": 0, + "url": "https://ge.xhamster.com/videos/college-spex-teen-facialized-in-dormroom-orgy-7793905", + "review": 0, + "should_download": 0, + "title": "College-Spex-Teen in der Schlafsaal-Orgie ins Gesicht gespritzt | xHamster", + "file_name": "College-Spex-Teen in der Schlafsaal-Orgie ins Gesicht gespritzt [7793905].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7d65d3d8-ddf6-44b4-887d-bfdc6d7b4256.mp4" + }, + { + "id": "7dbed932-89bd-41a0-9ca6-d3762c56e0ee", + "created_date": "2024-12-29 23:53:27.897986", + "last_modified_date": "2024-12-29 23:53:27.897986", + "version": 0, + "url": "https://ge.xhamster.com/videos/milf-giving-my-redhead-stepmom-the-creampie-she-deserves-13366137", + "review": 0, + "should_download": 0, + "title": "Milf, meiner rothaarigen Stiefmutter den Creampie geben, den sie verdient | xHamster", + "file_name": "Milf, meiner rothaarigen Stiefmutter den Creampie geben, den sie verdient [13366137].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/7dbed932-89bd-41a0-9ca6-d3762c56e0ee.mp4" + }, + { + "id": "7e389768-55ad-4413-82a5-070c5c1b5e20", + "created_date": "2024-07-25 07:29:45.278060", + "last_modified_date": "2024-07-25 07:29:45.278060", + "version": 0, + "url": "https://ge.xhamster.com/videos/gangbang-in-der-kueche-xhxCRXO", + "review": 0, + "should_download": 0, + "title": "Gangbang in Der Kueche, Free Brutal Sex Porn f2 | xHamster", + "file_name": "Gangbang in der Kueche [xhxCRXO].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7e389768-55ad-4413-82a5-070c5c1b5e20.mp4" + }, + { + "id": "7e3b65c7-ee77-4682-b94f-2cd592e80e4f", + "created_date": "2024-07-25 07:29:47.964894", + "last_modified_date": "2024-07-25 07:29:47.964894", + "version": 0, + "url": "https://ge.xhamster.com/videos/behind-the-scenes-at-our-office-14839388", + "review": 0, + "should_download": 0, + "title": "Hinter den Kulissen in unserem B\u00fcro | xHamster", + "file_name": "Hinter den Kulissen in unserem B\u00fcro [14839388].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7e3b65c7-ee77-4682-b94f-2cd592e80e4f.mp4" + }, + { + "id": "7e3befc3-04a9-4ab5-bdc5-0493de4d972b", + "created_date": "2024-07-25 07:29:45.494986", + "last_modified_date": "2024-07-25 07:29:45.494986", + "version": 0, + "url": "https://ge.xhamster.com/videos/perverse-minds-episode-01-xhThHGmF", + "review": 0, + "should_download": 0, + "title": "Perverse K\u00f6pfe !!! - Episode # 01 | xHamster", + "file_name": "Perverse K\u00f6pfe !!! - Episode # 01 [xhThHGmF].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7e3befc3-04a9-4ab5-bdc5-0493de4d972b.mp4" + }, + { + "id": "7e51ab34-fd4f-425f-b33e-8391d8fdb7d1", + "created_date": "2025-01-19 13:42:28.707422", + "last_modified_date": "2025-01-19 13:42:28.707429", + "version": 0, + "url": "https://ge.xhamster.com/videos/plenty-for-everyone-s45-e18-xhvXGBg", + "review": 0, + "should_download": 0, + "title": "Viel f\u00fcr jeden - S45: e18 | xHamster", + "file_name": "Viel f\u00fcr jeden - S45\uff1a e18 [xhvXGBg].mp4", + "path": null, + "cloud_link": null + }, + { + "id": "7e9bc820-6593-4efd-91ee-d67b2a0e34ea", + "created_date": "2024-07-25 07:29:45.438595", + "last_modified_date": "2024-07-25 07:29:45.438595", + "version": 0, + "url": "https://ge.xhamster.com/videos/amazing-french-blonde-milf-double-penetrated-at-the-beach-xhODl8H", + "review": 0, + "should_download": 0, + "title": "Erstaunliche franz\u00f6sische blonde MILF am strand doppelt penetriert | xHamster", + "file_name": "Erstaunliche franz\u00f6sische blonde MILF am strand doppelt penetriert [xhODl8H].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/7e9bc820-6593-4efd-91ee-d67b2a0e34ea.mp4" + }, + { + "id": "7ed3d5c0-11eb-401f-8965-206bffc0fa0d", + "created_date": "2024-08-28 23:21:54.350667", + "last_modified_date": "2024-08-28 23:21:54.350667", + "version": 0, + "url": "https://ge.xhamster.com/videos/wife-fucked-in-poker-game-12677727", + "review": 0, + "should_download": 0, + "title": "Ehefrau im Pokerspiel gefickt | xHamster", + "file_name": "Ehefrau im Pokerspiel gefickt [12677727].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/7ed3d5c0-11eb-401f-8965-206bffc0fa0d.mp4" + }, + { + "id": "7f0be3ba-f222-4540-9fdf-472a74a1524a", + "created_date": "2025-01-19 13:42:36.947901", + "last_modified_date": "2025-01-19 13:42:36.947908", + "version": 0, + "url": "https://ge.xhamster.com/videos/oops-my-stepmom-tripped-on-my-dick-again-jane-cane-shiny-cock-films-xhI8LhZ", + "review": 0, + "should_download": 0, + "title": "Ups, meine stiefmutter hat auf meinen schwanz gestopft! Wieder! Jane rohrstock, gl\u00e4nzende schwanzfilme | xHamster", + "file_name": "Ups, meine stiefmutter hat auf meinen schwanz gestopft! Wieder! Jane rohrstock, gl\u00e4nzende schwanzfilme [xhI8LhZ].mp4", + "path": null, + "cloud_link": null + }, + { + "id": "7f345d7f-51c1-4d9d-bf8f-79517a2da36c", + "created_date": "2025-01-17 16:34:28.620435", + "last_modified_date": "2025-01-17 16:34:28.620442", + "version": 0, + "url": "https://ge.xhamster.com/videos/catrice-xhAyCw2", + "review": 0, + "should_download": 0, + "title": "Catrice: Retro HD Porn Video ce | xHamster", + "file_name": "Catrice [xhAyCw2].mp4", + "path": null, + "cloud_link": "/data/media/7f345d7f-51c1-4d9d-bf8f-79517a2da36c.mp4" + }, + { + "id": "7f64a874-ae3d-4840-870c-a4677a919b3f", + "created_date": "2024-07-25 07:29:47.129060", + "last_modified_date": "2024-07-25 07:29:47.129060", + "version": 0, + "url": "https://ge.xhamster.com/videos/thick-new-secretary-holly-day-learns-the-free-use-office-rules-on-her-first-day-freeuse-fantasy-xhER46f", + "review": 0, + "should_download": 0, + "title": "Dicke neue sekret\u00e4rin Holly day lernt an ihrem ersten tag die freien b\u00fcroregeln - freeUse fantasy | xHamster", + "file_name": "Dicke neue sekret\u00e4rin Holly day lernt an ihrem ersten tag die freien b\u00fcroregeln - freeUse fantasy [xhER46f].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7f64a874-ae3d-4840-870c-a4677a919b3f.mp4" + }, + { + "id": "7f7b06ca-a46c-480e-bec5-c8866099a933", + "created_date": "2024-07-25 07:29:46.200150", + "last_modified_date": "2024-07-25 07:29:46.200150", + "version": 0, + "url": "https://ge.xhamster.com/videos/teenievision-22-schluckende-schwanz-goren-a-noi-piace-succhiare-den-frechen-goren-juckt-der-arsch-xhqx8Vz", + "review": 0, + "should_download": 0, + "title": "Teenievision 22 - Schluckende Schwanz-goren - a Noi Piace Succhiare - Den Frechen Goren Juckt Der Arsch | xHamster", + "file_name": "TeenieVision 22 - Schluckende Schwanz-Goren - A noi piace succhiare - Den frechen Goren juckt der Arsch [xhqx8Vz].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7f7b06ca-a46c-480e-bec5-c8866099a933.mp4" + }, + { + "id": "7f8ffb98-92b3-4ca6-8b40-44ae03bbb801", + "created_date": "2024-07-25 07:29:46.776290", + "last_modified_date": "2024-07-25 07:29:46.776290", + "version": 0, + "url": "https://ge.xhamster.com/videos/american-pure-pleasure-xxx-vol-04-xhizSQr", + "review": 0, + "should_download": 0, + "title": "Amerikanisch pures Vergn\u00fcgen xxx - vol. # 04 | xHamster", + "file_name": "Amerikanisch pures Vergn\u00fcgen xxx - vol. # 04 [xhizSQr].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7f8ffb98-92b3-4ca6-8b40-44ae03bbb801.mp4" + }, + { + "id": "7fa6fd73-6391-4ada-8eaf-b8a73cc01bc8", + "created_date": "2024-07-25 07:29:47.224581", + "last_modified_date": "2024-07-25 07:29:47.224581", + "version": 0, + "url": "https://ge.xhamster.com/videos/step-mom-fucked-by-stepson-xhaHIvE", + "review": 0, + "should_download": 0, + "title": "Stiefmutter vom Stiefsohn gefickt | xHamster", + "file_name": "Stiefmutter vom Stiefsohn gefickt [xhaHIvE].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/7fa6fd73-6391-4ada-8eaf-b8a73cc01bc8.mp4" + }, + { + "id": "7ffcc84e-df73-4254-acd6-675fabd29d42", + "created_date": "2024-08-16 12:29:27.802000", + "last_modified_date": "2024-10-21 16:30:07.121000", + "version": 1, + "url": "https://ge.xhamster.com/videos/perverted-sex-games-1977-xhNUlrP", + "review": 0, + "should_download": 0, + "title": "Perverse Sexspiele (1977) | xHamster", + "file_name": "Perverse Sexspiele (1977) [xhNUlrP].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/7ffcc84e-df73-4254-acd6-675fabd29d42.mp4" + }, + { + "id": "8056eaa6-04e3-40ad-a8ad-be2e0bf57516", + "created_date": "2024-07-25 07:29:46.874117", + "last_modified_date": "2024-07-25 07:29:46.874117", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsiblings-orgy-fuck-in-front-of-step-mom-myfamilypies-s3-e4-10114327", + "review": 0, + "should_download": 0, + "title": "Stiefgeschwister-Orgie ficken vor Stiefmutter - myfamilypies s3: e4 | xHamster", + "file_name": "Stiefgeschwister-Orgie ficken vor Stiefmutter - myfamilypies s3\uff1a e4 [10114327].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8056eaa6-04e3-40ad-a8ad-be2e0bf57516.mp4" + }, + { + "id": "80fe6acd-b32b-45d1-9ba1-e189a41b43cf", + "created_date": "2024-11-10 16:53:33.486690", + "last_modified_date": "2024-11-10 16:53:33.486690", + "version": 0, + "url": "https://ge.xhamster.com/videos/after-the-wedding-2017-xhn5MhG", + "review": 0, + "should_download": 0, + "title": "Nach der Hochzeit 2017 | xHamster", + "file_name": "Nach der Hochzeit 2017 [xhn5MhG].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/80fe6acd-b32b-45d1-9ba1-e189a41b43cf.mp4" + }, + { + "id": "812e57ff-4ec1-4315-9f26-876f0fb8b739", + "created_date": "2024-07-25 07:29:47.506055", + "last_modified_date": "2024-07-25 07:29:47.506055", + "version": 0, + "url": "https://ge.xhamster.com/videos/classic-artist-s-tale-xhbNuX4", + "review": 0, + "should_download": 0, + "title": "Klassische K\u00fcnstlergeschichte | xHamster", + "file_name": "Klassische K\u00fcnstlergeschichte [xhbNuX4].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/812e57ff-4ec1-4315-9f26-876f0fb8b739.mp4" + }, + { + "id": "81dc2252-aefe-4f0d-8084-5a16523fda0c", + "created_date": "2024-07-25 07:29:47.307387", + "last_modified_date": "2024-07-25 07:29:47.307387", + "version": 0, + "url": "https://ge.xhamster.com/videos/swimming-pool-orgy-at-czech-mega-swingers-1081009", + "review": 0, + "should_download": 0, + "title": "Schwimmbad-Orgie bei tschechischen Mega-Swingern | xHamster", + "file_name": "Schwimmbad-Orgie bei tschechischen Mega-Swingern [1081009].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/81dc2252-aefe-4f0d-8084-5a16523fda0c.mp4" + }, + { + "id": "8204a76a-be81-45a1-9847-e2cbdcbfc7e2", + "created_date": "2024-07-25 07:29:45.316467", + "last_modified_date": "2024-07-25 07:29:45.316467", + "version": 0, + "url": "https://ge.xhamster.com/videos/griechische-liebesnaechte-1984-9092728", + "review": 0, + "should_download": 0, + "title": "Griechische Liebesnaechte 1984, Free Teen Porn a9 | xHamster", + "file_name": "Griechische Liebesnaechte (1984) [9092728].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8204a76a-be81-45a1-9847-e2cbdcbfc7e2.mp4" + }, + { + "id": "8227283d-7c12-4894-bd63-8c0fa29f6837", + "created_date": "2024-09-24 08:11:39.000933", + "last_modified_date": "2024-10-21 16:30:14.035000", + "version": 1, + "url": "https://ge.xhamster.com/videos/family-strokes-buxom-bombshell-milf-joins-her-stepson-and-his-sexy-gf-while-banging-on-the-couch-xhWhtRz", + "review": 0, + "should_download": 0, + "title": "In Family Strokes, einer drallen MILF, schlie\u00dft sich ihr MILF ihrem Stiefsohn und seiner sexy Freundin an, w\u00e4hrend sie auf der Couch knallt | xHamster", + "file_name": "In Family Strokes, einer drallen MILF, schlie\u00dft sich ihr MILF ihrem Stiefsohn und seiner sexy Freundin an, w\u00e4hrend sie auf der Couch knallt [xhWhtRz].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/8227283d-7c12-4894-bd63-8c0fa29f6837.mp4" + }, + { + "id": "824b0de1-6db2-43f6-a8e9-7948014c5df8", + "created_date": "2024-07-25 07:29:47.631349", + "last_modified_date": "2024-07-25 07:29:47.631349", + "version": 0, + "url": "https://ge.xhamster.com/videos/american-college-xxx-full-movie-hd-original-version-xhzrtLPK", + "review": 0, + "should_download": 0, + "title": "American College xxx - (kompletter Film hd Originalversion) | xHamster", + "file_name": "American College xxx - (kompletter Film hd Originalversion) [xhzrtLPK].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/824b0de1-6db2-43f6-a8e9-7948014c5df8.mp4" + }, + { + "id": "829a359b-e36b-4018-8f20-e91de3b53592", + "created_date": "2024-07-25 07:29:45.522181", + "last_modified_date": "2024-07-25 07:29:45.522181", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-hot-blowjobs-of-the-naughty-nurse-xhYi9qF", + "review": 0, + "should_download": 0, + "title": "Die hei\u00dfen Blowjobs der frechen Krankenschwester | xHamster", + "file_name": "Die hei\u00dfen Blowjobs der frechen Krankenschwester [xhYi9qF].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/829a359b-e36b-4018-8f20-e91de3b53592.mp4" + }, + { + "id": "83e7f55f-a8ad-4ebf-b60f-d41ce157d97c", + "created_date": "2024-08-28 23:21:54.360773", + "last_modified_date": "2024-08-28 23:21:54.360773", + "version": 0, + "url": "https://ge.xhamster.com/videos/young-guy-gambles-his-cute-teen-girlfriend-at-gambling-xhtuSZ2", + "review": 0, + "should_download": 0, + "title": "Junger Typ verzockt seine niedliche Teenie Freundin beim Gl\u00fccksspiel | xHamster", + "file_name": "Junger Typ verzockt seine niedliche Teenie Freundin beim Gl\u00fccksspiel [xhtuSZ2].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/83e7f55f-a8ad-4ebf-b60f-d41ce157d97c.mp4" + }, + { + "id": "83f7b829-4210-4f66-bc1a-b3c1e1ad3122", + "created_date": "2024-08-09 21:05:39.819412", + "last_modified_date": "2024-08-16 10:31:14.624000", + "version": 1, + "url": "https://ge.xhamster.com/videos/sneaky-voyeurs-jenga-game-gets-naughty-seduction-leads-to-intense-finale-xhTTHna", + "review": 0, + "should_download": 0, + "title": "Hinterh\u00e4ltige voyeurs: Jenga-Spiel wird frech, Verf\u00fchrung f\u00fchrt zu intensivem finale | xHamster", + "file_name": "Hinterh\u00e4ltige voyeurs\uff1a Jenga-Spiel wird frech, Verf\u00fchrung f\u00fchrt zu intensivem finale [xhTTHna].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/83f7b829-4210-4f66-bc1a-b3c1e1ad3122.mp4" + }, + { + "id": "83fabb38-655b-41ad-b4ef-ce4acbdb29e2", + "created_date": "2024-07-25 07:29:46.242353", + "last_modified_date": "2024-07-25 07:29:46.242353", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepmom-helps-me-loosen-up-s8-e9-xhR8r1n", + "review": 0, + "should_download": 0, + "title": "Stiefmutter hilft mir, mich zu lockern - s8: e9 | xHamster", + "file_name": "Stiefmutter hilft mir, mich zu lockern - s8\uff1a e9 [xhR8r1n].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/83fabb38-655b-41ad-b4ef-ce4acbdb29e2.mp4" + }, + { + "id": "841e00c7-abb2-47c4-b978-52a87bbdde2b", + "created_date": "2024-07-25 07:29:45.415535", + "last_modified_date": "2024-07-25 07:29:45.415535", + "version": 0, + "url": "https://ge.xhamster.com/videos/beer-pong-and-blowjobs-1480121", + "review": 0, + "should_download": 0, + "title": "Bier-Pong und Blowjobs | xHamster", + "file_name": "Bier-Pong und Blowjobs [1480121].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/841e00c7-abb2-47c4-b978-52a87bbdde2b.mp4" + }, + { + "id": "842c8c66-2a4e-40fc-b50b-cd946e6dd218", + "created_date": "2024-07-25 07:29:48.048519", + "last_modified_date": "2024-07-25 07:29:48.048519", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-little-stepsister-loves-to-suck-my-big-cock-karolinaorgasm-xhnEhiE", + "review": 0, + "should_download": 0, + "title": "Meine kleine Stiefschwester liebt es, meinen gro\u00dfen Schwanz zu lutschen - Karolinaorgasm | xHamster", + "file_name": "Meine kleine Stiefschwester liebt es, meinen gro\u00dfen Schwanz zu lutschen - Karolinaorgasm [xhnEhiE].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/842c8c66-2a4e-40fc-b50b-cd946e6dd218.mp4" + }, + { + "id": "843622b2-1f8b-43a9-8b57-f51fcd62de81", + "created_date": "2024-07-25 07:29:45.064070", + "last_modified_date": "2024-07-25 07:29:45.064070", + "version": 0, + "url": "https://ge.xhamster.com/videos/forte-immerscharf8-6612071", + "review": 0, + "should_download": 0, + "title": "Forte Immerscharf8: Free Mature Porn Video 70 | xHamster", + "file_name": "FORTE Immerscharf8 [6612071].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/843622b2-1f8b-43a9-8b57-f51fcd62de81.mp4" + }, + { + "id": "846c7325-aaff-4a46-856a-090668ef826d", + "created_date": "2024-07-25 07:29:44.684934", + "last_modified_date": "2024-07-25 07:29:44.684934", + "version": 0, + "url": "https://ge.xhamster.com/videos/perversum-chateau-14260395", + "review": 0, + "should_download": 0, + "title": "Perversum Chateau: Free European Porn Video 8e | xHamster", + "file_name": "Perversum Chateau [14260395].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/846c7325-aaff-4a46-856a-090668ef826d.mp4" + }, + { + "id": "84708b5f-0822-4dc8-a499-407c10f92246", + "created_date": "2024-07-25 07:29:46.817533", + "last_modified_date": "2024-07-25 07:29:46.817533", + "version": 0, + "url": "https://ge.xhamster.com/videos/nasty-german-housewives-xhM0cH3", + "review": 0, + "should_download": 0, + "title": "Versaute deutsche Hausfrauen | xHamster", + "file_name": "Versaute deutsche Hausfrauen [xhM0cH3].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/84708b5f-0822-4dc8-a499-407c10f92246.mp4" + }, + { + "id": "852a058b-731b-4c7f-8616-c245865e4303", + "created_date": "2024-07-25 07:29:45.449697", + "last_modified_date": "2024-07-25 07:29:45.449697", + "version": 0, + "url": "https://ge.xhamster.com/videos/teenie-parade-15-full-movie-xh7GDGk", + "review": 0, + "should_download": 0, + "title": "Teenie-Parade 15 (kompletter Film) | xHamster", + "file_name": "Teenie-Parade 15 (kompletter Film) [xh7GDGk].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/852a058b-731b-4c7f-8616-c245865e4303.mp4" + }, + { + "id": "853a389d-c1f7-43cd-a4d6-e99aa6cba67c", + "created_date": "2024-07-25 07:29:45.040235", + "last_modified_date": "2024-07-25 07:29:45.040235", + "version": 0, + "url": "https://ge.xhamster.com/videos/literaturquintett-nicht-ohne-meine-locher-1995-xhFxThW", + "review": 0, + "should_download": 0, + "title": "Literaturquintett Nicht Ohne Meine Locher 1995: Porn 11 | xHamster", + "file_name": "Literaturquintett\uff1a Nicht ohne meine Locher (1995) [xhFxThW].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/853a389d-c1f7-43cd-a4d6-e99aa6cba67c.mp4" + }, + { + "id": "85c2f039-d5d0-463a-bfaf-392ee0b7b51a", + "created_date": "2024-09-24 08:11:39.006101", + "last_modified_date": "2024-10-21 16:30:19.990000", + "version": 1, + "url": "https://ge.xhamster.com/videos/finally-18-the-first-time-xhcsjFh", + "review": 0, + "should_download": 0, + "title": "Endlich 18! Das erste Mal! | xHamster", + "file_name": "Endlich 18! Das erste Mal! [xhcsjFh].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/85c2f039-d5d0-463a-bfaf-392ee0b7b51a.mp4" + }, + { + "id": "85d7b763-dd13-4afa-a082-ae2873391a61", + "created_date": "2024-11-01 21:08:26.937271", + "last_modified_date": "2024-11-01 21:08:26.937271", + "version": 0, + "url": "https://ge.xhamster.com/videos/die-arschfickwirtin-4613032", + "review": 0, + "should_download": 0, + "title": "Die Arschfickwirtin: Free Anal Porn Video a8 | xHamster", + "file_name": "Die Arschfickwirtin [4613032].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/85d7b763-dd13-4afa-a082-ae2873391a61.mp4" + }, + { + "id": "85f9b91a-fbff-44d8-bf94-04e147f66016", + "created_date": "2024-07-25 07:29:46.718820", + "last_modified_date": "2024-07-25 07:29:46.718820", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsis-says-bet-you-never-thought-your-stepmom-would-be-sucking-your-cock-xhm3g2n", + "review": 0, + "should_download": 0, + "title": "Stiefschwester sagt, wetten, du h\u00e4ttest nie gedacht, dass deine Stiefmutter deinen Schwanz lutscht | xHamster", + "file_name": "Stiefschwester sagt, wetten, du h\u00e4ttest nie gedacht, dass deine Stiefmutter deinen Schwanz lutscht [xhm3g2n].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/85f9b91a-fbff-44d8-bf94-04e147f66016.mp4" + }, + { + "id": "86027361-7d40-488e-be07-b013ec5756bb", + "created_date": "2024-07-25 07:29:45.547650", + "last_modified_date": "2024-07-25 07:29:45.547650", + "version": 0, + "url": "https://ge.xhamster.com/videos/horny-hot-milf-cory-chase-empties-her-stepson-s-cock-xhj5cc5", + "review": 0, + "should_download": 0, + "title": "Geile hei\u00dfe MILF Cory Chase leert den Schwanz ihres Stiefsohns | xHamster", + "file_name": "Geile hei\u00dfe MILF Cory Chase leert den Schwanz ihres Stiefsohns [xhj5cc5].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/86027361-7d40-488e-be07-b013ec5756bb.mp4" + }, + { + "id": "861ea1f3-c3b2-47c5-ae48-2af2715d23a9", + "created_date": "2024-12-30 18:49:39.874000", + "last_modified_date": "2025-01-03 01:46:10.678000", + "version": 2, + "url": "https://ge.xhamster.com/videos/stepbrother-and-stepsister-seduce-drunk-stepmom-into-threesome-14171473", + "review": 0, + "should_download": 0, + "title": "Stiefbruder und Schwester verf\u00fchren Mutter zum Dreier | xHamster", + "file_name": "Stiefbruder und Schwester verf\u00fchren Mutter zum Dreier [14171473].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/861ea1f3-c3b2-47c5-ae48-2af2715d23a9.mp4" + }, + { + "id": "8656dc50-8003-4a4d-90b7-063633bc7a05", + "created_date": "2024-07-25 07:29:44.624306", + "last_modified_date": "2024-07-25 07:29:44.624306", + "version": 0, + "url": "https://ge.xhamster.com/videos/old-german-porn-9756751", + "review": 0, + "should_download": 0, + "title": "Alter deutscher Porno. | xHamster", + "file_name": "Alter deutscher Porno. [9756751].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8656dc50-8003-4a4d-90b7-063633bc7a05.mp4" + }, + { + "id": "865ed219-bcf0-45c6-b160-e11c5efdf115", + "created_date": "2024-07-25 07:29:45.375122", + "last_modified_date": "2024-07-25 07:29:45.375122", + "version": 0, + "url": "https://ge.xhamster.com/videos/all-made-in-family-xhPaPlS", + "review": 0, + "should_download": 0, + "title": "Alles in familie gemacht | xHamster", + "file_name": "Alles in familie gemacht [xhPaPlS].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/865ed219-bcf0-45c6-b160-e11c5efdf115.mp4" + }, + { + "id": "86704785-3a27-4ffc-81c9-f218cfef6522", + "created_date": "2024-07-25 07:29:44.875893", + "last_modified_date": "2024-07-25 07:29:44.875893", + "version": 0, + "url": "https://ge.xhamster.com/videos/happy-birthday-capri-xhsvSxW", + "review": 0, + "should_download": 0, + "title": "Alles Gute zum Geburtstag, Capri | xHamster", + "file_name": "Alles Gute zum Geburtstag, Capri [xhsvSxW].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/86704785-3a27-4ffc-81c9-f218cfef6522.mp4" + }, + { + "id": "8690d732-3709-485f-9c7d-98f9367c5f72", + "created_date": "2024-07-25 07:29:47.862846", + "last_modified_date": "2024-07-25 07:29:47.862846", + "version": 0, + "url": "https://ge.xhamster.com/videos/wie-rettet-man-eine-ehe-1976-with-patricia-rhomberg-6182432", + "review": 0, + "should_download": 0, + "title": "Wie Rettet Man Eine Ehe 1976 with Patricia Rhomberg | xHamster", + "file_name": "Wie Rettet Man Eine Ehe (1976) with Patricia Rhomberg [6182432].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8690d732-3709-485f-9c7d-98f9367c5f72.mp4" + }, + { + "id": "86cd7994-6c59-4714-aecb-1a89d778f168", + "created_date": "2024-07-25 07:29:45.839405", + "last_modified_date": "2024-07-25 07:29:45.839405", + "version": 0, + "url": "https://ge.xhamster.com/videos/step-mom-got-stretched-out-by-3-great-whites-11405640", + "review": 0, + "should_download": 0, + "title": "Stiefmutter wurde von 3 tollen Wei\u00dfen gestreckt | xHamster", + "file_name": "Stiefmutter wurde von 3 tollen Wei\u00dfen gestreckt [11405640].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/86cd7994-6c59-4714-aecb-1a89d778f168.mp4" + }, + { + "id": "8731317c-2251-44db-88a5-94cf1ad12f85", + "created_date": "2024-09-05 20:03:45.850222", + "last_modified_date": "2024-10-21 16:30:24.505000", + "version": 1, + "url": "https://ge.xhamster.com/videos/classic-eighties-vintage-11-4049136", + "review": 0, + "should_download": 0, + "title": "Klassischer Retro der 80er Jahre | xHamster", + "file_name": "Klassischer Retro der 80er Jahre [4049136].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/8731317c-2251-44db-88a5-94cf1ad12f85.mp4" + }, + { + "id": "873da7af-13e8-4c2e-850c-1c04bc21383e", + "created_date": "2024-07-25 07:29:47.854553", + "last_modified_date": "2024-07-25 07:29:47.854553", + "version": 0, + "url": "https://ge.xhamster.com/videos/perversion-in-ibiza-full-movie-original-in-full-hd-xhJHHlR", + "review": 0, + "should_download": 0, + "title": "Perversion in Ibiza - (kompletter Film) - (Original in Full HD) | xHamster", + "file_name": "Perversion in Ibiza - (kompletter Film) - (Original in Full HD) [xhJHHlR].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/873da7af-13e8-4c2e-850c-1c04bc21383e.mp4" + }, + { + "id": "87662bfa-edd2-4377-956a-49562f67213a", + "created_date": "2024-07-25 07:29:44.562585", + "last_modified_date": "2024-07-25 07:29:44.562585", + "version": 0, + "url": "https://ge.xhamster.com/videos/familystrokes-stepfamily-taboo-orgy-with-stepdaughter-judy-jolie-and-busty-stepmom-becky-bandini-xh6VzN1", + "review": 0, + "should_download": 0, + "title": "Familystrokes - Stieffamilien-Tabu-Orgie mit Stieftochter Judy Jolie und vollbusiger Stiefmutter Becky Bandini | xHamster", + "file_name": "Familystrokes - Stieffamilien-Tabu-Orgie mit Stieftochter Judy Jolie und vollbusiger Stiefmutter Becky Bandini [xh6VzN1].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/87662bfa-edd2-4377-956a-49562f67213a.mp4" + }, + { + "id": "87b25060-41f8-40bd-98aa-7b0733ab930f", + "created_date": "2024-08-28 23:21:54.363542", + "last_modified_date": "2024-08-28 23:21:54.363542", + "version": 0, + "url": "https://ge.xhamster.com/videos/it-s-good-having-slutty-friends-to-fuck-with-xhW3zI4", + "review": 0, + "should_download": 0, + "title": "Es ist gut, mit versauten Freunden zu ficken | xHamster", + "file_name": "Es ist gut, mit versauten Freunden zu ficken [xhW3zI4].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/87b25060-41f8-40bd-98aa-7b0733ab930f.mp4" + }, + { + "id": "87f2e8af-fe90-45b6-ab80-56c6595cc3ae", + "created_date": "2024-07-25 07:29:47.642946", + "last_modified_date": "2024-07-25 07:29:47.642946", + "version": 0, + "url": "https://ge.xhamster.com/videos/vintage-orgy-138-11414198", + "review": 0, + "should_download": 0, + "title": "Retro-Orgie 138 | xHamster", + "file_name": "Retro-Orgie 138 [11414198].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/87f2e8af-fe90-45b6-ab80-56c6595cc3ae.mp4" + }, + { + "id": "88054bed-48a6-46d2-b8e8-b22546689627", + "created_date": "2025-01-17 16:34:33.254526", + "last_modified_date": "2025-01-17 16:34:33.254532", + "version": 0, + "url": "https://ge.xhamster.com/videos/naomi-walker-step-mom-and-daugther-fuck-by-3-studs-10238796", + "review": 0, + "should_download": 0, + "title": "In Naomi Walker ficken Stiefmutter und Tochter von 3 Hengsten | xHamster", + "file_name": "In Naomi Walker ficken Stiefmutter und Tochter von 3 Hengsten [10238796].mp4", + "path": null, + "cloud_link": "/data/media/88054bed-48a6-46d2-b8e8-b22546689627.mp4" + }, + { + "id": "8836c945-da32-49ed-aa3b-fca5fb75dcf9", + "created_date": "2024-07-25 07:29:47.775844", + "last_modified_date": "2024-07-25 07:29:47.775844", + "version": 0, + "url": "https://ge.xhamster.com/videos/busty-alexis-fawx-fucking-her-boss-in-the-office-14838792", + "review": 0, + "should_download": 0, + "title": "Die vollbusige Alexis Fawx fickt ihren Chef im B\u00fcro | xHamster", + "file_name": "Die vollbusige Alexis Fawx fickt ihren Chef im B\u00fcro [14838792].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8836c945-da32-49ed-aa3b-fca5fb75dcf9.mp4" + }, + { + "id": "88564351-085c-4335-8e26-5dd807409b2c", + "created_date": "2024-07-25 07:29:47.093079", + "last_modified_date": "2024-07-25 07:29:47.093079", + "version": 0, + "url": "https://ge.xhamster.com/videos/boarding-school-full-movie-xhndz9Q", + "review": 0, + "should_download": 0, + "title": "Internat (kompletter Film) | xHamster", + "file_name": "Internat (kompletter Film) [xhndz9Q].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/88564351-085c-4335-8e26-5dd807409b2c.mp4" + }, + { + "id": "8915e465-8442-4557-b917-4f6deb6289ec", + "created_date": "2024-07-25 07:29:45.518428", + "last_modified_date": "2024-07-25 07:29:45.518428", + "version": 0, + "url": "https://ge.xhamster.com/videos/little-stepbrother-caught-wanking-and-deflowered-xhXevD6", + "review": 0, + "should_download": 0, + "title": "Kleiner Bruder beim Wichsen erwischt und entjungfert | xHamster", + "file_name": "Kleiner Bruder beim Wichsen erwischt und entjungfert [xhXevD6].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8915e465-8442-4557-b917-4f6deb6289ec.mp4" + }, + { + "id": "89617489-2e4d-49e3-b5e1-ed475fdc9e3a", + "created_date": "2024-07-25 07:29:45.613085", + "last_modified_date": "2024-07-25 07:29:45.613085", + "version": 0, + "url": "https://ge.xhamster.com/videos/wife-gets-woken-up-by-husband-and-his-friend-then-fucks-both-of-them-xhePvGw", + "review": 0, + "should_download": 0, + "title": "Die Ehefrau wird von Ehemann und seinem Freund geweckt und fickt dann beide | xHamster", + "file_name": "Die Ehefrau wird von Ehemann und seinem Freund geweckt und fickt dann beide [xhePvGw].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/89617489-2e4d-49e3-b5e1-ed475fdc9e3a.mp4" + }, + { + "id": "89d5a590-fe2d-404e-914a-89b0a024db45", + "created_date": "2024-07-25 07:29:45.149988", + "last_modified_date": "2024-07-25 07:29:45.149988", + "version": 0, + "url": "https://ge.xhamster.com/videos/stief-mutter-erwischt-tante-mit-sohn-und-fickt-mit-8899974", + "review": 0, + "should_download": 0, + "title": "Stief-mutter Erwischt Tante Mit Sohn Und Fickt Mit: Porn 54 | xHamster", + "file_name": "Stief-Mutter erwischt Tante mit Sohn und fickt mit [8899974].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/89d5a590-fe2d-404e-914a-89b0a024db45.mp4" + }, + { + "id": "89ea68d6-8de3-4dda-aefc-41e1a1371a2c", + "created_date": "2024-07-25 07:29:44.681415", + "last_modified_date": "2024-07-25 07:29:44.681415", + "version": 0, + "url": "https://ge.xhamster.com/videos/casey-calvert-gets-assbanged-by-black-guys-5309556", + "review": 0, + "should_download": 0, + "title": "Casey Calvert wird von Schwarzen gefickt | xHamster", + "file_name": "Casey Calvert wird von Schwarzen gefickt [5309556].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/89ea68d6-8de3-4dda-aefc-41e1a1371a2c.mp4" + }, + { + "id": "8a75a960-02d8-4ade-abff-e889725b117e", + "created_date": "2024-07-25 07:29:47.616386", + "last_modified_date": "2024-07-25 07:29:47.616386", + "version": 0, + "url": "https://ge.xhamster.com/videos/urlaubsflirts-full-movie-xhMVuEm", + "review": 0, + "should_download": 0, + "title": "Urlaubsflirts Full Movie, Free Story HD Porn 07 | xHamster", + "file_name": "Urlaubsflirts (Full Movie) [xhMVuEm].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8a75a960-02d8-4ade-abff-e889725b117e.mp4" + }, + { + "id": "8b046e5d-3d6b-4380-816e-69b6e516b78e", + "created_date": "2024-12-29 23:53:27.901150", + "last_modified_date": "2024-12-29 23:53:27.901150", + "version": 0, + "url": "https://ge.xhamster.com/videos/grenzenloser-deutscher-sex-full-movie-xhlEt9E", + "review": 0, + "should_download": 0, + "title": "Grenzenloser Deutscher Sex Full Movie, Porn 05 | xHamster", + "file_name": "Grenzenloser deutscher Sex! (Full Movie) [xhlEt9E].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/8b046e5d-3d6b-4380-816e-69b6e516b78e.mp4" + }, + { + "id": "8b47ac62-1c85-4747-a84b-0e448464c4d6", + "created_date": "2024-07-25 07:29:44.586155", + "last_modified_date": "2024-07-25 07:29:44.586155", + "version": 0, + "url": "https://ge.xhamster.com/videos/mother-gets-dp-from-two-young-boys-xhLJ0aU", + "review": 0, + "should_download": 0, + "title": "Mutter bekommt Doppelpenetration von zwei kleinen Jungs | xHamster", + "file_name": "Mutter bekommt Doppelpenetration von zwei kleinen Jungs [xhLJ0aU].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8b47ac62-1c85-4747-a84b-0e448464c4d6.mp4" + }, + { + "id": "8b846460-0a3f-48a6-90a9-bcd91081ff0f", + "created_date": "2024-07-25 07:29:47.889399", + "last_modified_date": "2024-07-25 07:29:47.889399", + "version": 0, + "url": "https://ge.xhamster.com/videos/nzest-skandale-verbotene-10656719", + "review": 0, + "should_download": 0, + "title": "nzest Skandale Verbotene | xHamster", + "file_name": "nzest Skandale Verbotene [10656719].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8b846460-0a3f-48a6-90a9-bcd91081ff0f.mp4" + }, + { + "id": "8b9c77e3-45bf-4446-b4f9-1e7cd63fa28a", + "created_date": "2024-07-25 07:29:44.658867", + "last_modified_date": "2024-07-25 07:29:44.658867", + "version": 0, + "url": "https://ge.xhamster.com/videos/sex-teacher-julia-ann-fucking-4185459", + "review": 0, + "should_download": 0, + "title": "Sexlehrerin Julia Ann fickt | xHamster", + "file_name": "Sexlehrerin Julia Ann fickt [4185459].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8b9c77e3-45bf-4446-b4f9-1e7cd63fa28a.mp4" + }, + { + "id": "8bc8ddd0-81cd-4a5a-8394-368b5e7ea3ee", + "created_date": "2024-07-25 07:29:46.399811", + "last_modified_date": "2024-07-25 07:29:46.399811", + "version": 0, + "url": "https://ge.xhamster.com/videos/joi-roommate-s42-e16-xhgSjgG", + "review": 0, + "should_download": 0, + "title": "Wichsanleitung, WG - s42: e16 | xHamster", + "file_name": "Wichsanleitung, WG - s42\uff1a e16 [xhgSjgG].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8bc8ddd0-81cd-4a5a-8394-368b5e7ea3ee.mp4" + }, + { + "id": "8bd420af-f78d-48a5-ae66-e31ed5cea46d", + "created_date": "2024-07-25 07:29:45.532774", + "last_modified_date": "2024-07-25 07:29:45.532774", + "version": 0, + "url": "https://ge.xhamster.com/videos/familien-sex-schatten-der-bluschande-2190111", + "review": 0, + "should_download": 0, + "title": "Familien Sex Schatten Der Bluschande, Porn 2c | xHamster", + "file_name": "Familien Sex Schatten Der Bluschande [2190111].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/8bd420af-f78d-48a5-ae66-e31ed5cea46d.mp4" + }, + { + "id": "8bef162a-1814-4603-8b86-79ccb4f1ef6b", + "created_date": "2024-09-24 08:11:39.008415", + "last_modified_date": "2024-10-21 16:30:29.995000", + "version": 1, + "url": "https://ge.xhamster.com/videos/sons-friend-gives-the-milf-a-surprise-xh8lb1G", + "review": 0, + "should_download": 0, + "title": "Der Freund des Sohnes \u00fcberrascht die MILF | xHamster", + "file_name": "Der Freund des Sohnes \u00fcberrascht die MILF [xh8lb1G].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/8bef162a-1814-4603-8b86-79ccb4f1ef6b.mp4" + }, + { + "id": "8c27327e-6931-4b17-8ced-2dc1203c6e6d", + "created_date": "2024-07-25 07:29:46.994188", + "last_modified_date": "2024-07-25 07:29:46.994188", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-thighs-10437025", + "review": 0, + "should_download": 0, + "title": "Familienschenkel | xHamster", + "file_name": "Familienschenkel [10437025].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8c27327e-6931-4b17-8ced-2dc1203c6e6d.mp4" + }, + { + "id": "8c76da37-7205-4fcb-abad-58f0867a0fe7", + "created_date": "2024-07-25 07:29:45.142775", + "last_modified_date": "2024-09-06 09:41:16.095000", + "version": 1, + "url": "https://ge.xhamster.com/videos/verbotene-familien-spiele-1257-7093171", + "review": 0, + "should_download": 0, + "title": "Verbotene Familien spiele", + "file_name": "Verbotene Familien spiele 1257 [7093171].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8c76da37-7205-4fcb-abad-58f0867a0fe7.mp4" + }, + { + "id": "8cefff34-0c21-46bc-85ae-775aab0d5be8", + "created_date": "2024-07-25 07:29:45.055855", + "last_modified_date": "2024-07-25 07:29:45.055855", + "version": 0, + "url": "https://ge.xhamster.com/videos/mutti-ist-die-beste-3227797", + "review": 0, + "should_download": 0, + "title": "Mutti Ist Die Beste: Free Taboo Porn Video cd | xHamster", + "file_name": "Mutti ist die Beste [3227797].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8cefff34-0c21-46bc-85ae-775aab0d5be8.mp4" + }, + { + "id": "8dc5f613-6c04-4b68-8077-df82638cbbd0", + "created_date": "2024-07-25 07:29:44.775762", + "last_modified_date": "2024-07-25 07:29:44.775762", + "version": 0, + "url": "https://ge.xhamster.com/videos/familystrokes-hot-big-tit-aunt-helps-me-cum-7579336", + "review": 0, + "should_download": 0, + "title": "Familystrokes - eine hei\u00dfe Tante mit dicken Titten hilft mir beim Abspritzen | xHamster", + "file_name": "Familystrokes - eine hei\u00dfe Tante mit dicken Titten hilft mir beim Abspritzen [7579336].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8dc5f613-6c04-4b68-8077-df82638cbbd0.mp4" + }, + { + "id": "8dddecb4-0da7-4cb3-954a-3eaccd8910f7", + "created_date": "2024-07-25 07:29:44.910423", + "last_modified_date": "2024-07-25 07:29:44.910423", + "version": 0, + "url": "https://ge.xhamster.com/videos/18-videoz-threesome-with-two-cumshots-7129909", + "review": 0, + "should_download": 0, + "title": "18 Videoz - Dreier mit zwei Cumshots | xHamster", + "file_name": "18 Videoz - Dreier mit zwei Cumshots [7129909].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8dddecb4-0da7-4cb3-954a-3eaccd8910f7.mp4" + }, + { + "id": "8e672eaf-cf60-4f1f-817f-fef92bd940dc", + "created_date": "2024-10-21 15:08:43.556675", + "last_modified_date": "2024-10-21 16:30:34.407000", + "version": 1, + "url": "https://ge.xhamster.com/videos/shes-for-lunch-6244676", + "review": 0, + "should_download": 0, + "title": "Sie ist zum Mittagessen | xHamster", + "file_name": "Sie ist zum Mittagessen [6244676].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/8e672eaf-cf60-4f1f-817f-fef92bd940dc.mp4" + }, + { + "id": "8e7f890d-f124-480e-b020-8b7e8cce7caa", + "created_date": "2024-07-25 07:29:47.109941", + "last_modified_date": "2024-07-25 07:29:47.109941", + "version": 0, + "url": "https://ge.xhamster.com/videos/husband-watches-wife-get-fucked-by-a-stranger-xhsD91B", + "review": 0, + "should_download": 0, + "title": "Ehemann beobachtet, wie seine Ehefrau von einem Fremden gefickt wird | xHamster", + "file_name": "Ehemann beobachtet, wie seine Ehefrau von einem Fremden gefickt wird [xhsD91B].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8e7f890d-f124-480e-b020-8b7e8cce7caa.mp4" + }, + { + "id": "8e7fcbb7-fcfc-43f6-815d-7697d782c213", + "created_date": "2024-07-25 07:29:47.847145", + "last_modified_date": "2024-07-25 07:29:47.847145", + "version": 0, + "url": "https://ge.xhamster.com/videos/familienausflug-auf-dem-campingplatz-10805450", + "review": 0, + "should_download": 0, + "title": "Familienausflug Auf Dem Campingplatz, Porn 23 | xHamster", + "file_name": "Familienausflug auf dem Campingplatz [10805450].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8e7fcbb7-fcfc-43f6-815d-7697d782c213.mp4" + }, + { + "id": "8edd1307-9ee7-4a5f-860a-f9ff643f9192", + "created_date": "2024-07-25 07:29:44.484260", + "last_modified_date": "2024-07-25 07:29:44.484260", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepdads-teach-their-stepdaughters-how-to-fuck-daughterswap-xhpj6m7", + "review": 0, + "should_download": 0, + "title": "Stiefv\u00e4ter bringen ihren stieft\u00f6chtern bei, wie man fickt - daughterswap | xHamster", + "file_name": "Stiefv\u00e4ter bringen ihren stieft\u00f6chtern bei, wie man fickt - daughterswap [xhpj6m7].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/8edd1307-9ee7-4a5f-860a-f9ff643f9192.mp4" + }, + { + "id": "8eed03f1-b536-4451-ab44-699b51865857", + "created_date": "2024-07-25 07:29:48.037433", + "last_modified_date": "2024-07-25 07:29:48.037433", + "version": 0, + "url": "https://ge.xhamster.com/videos/tushy-first-double-penetration-for-natasha-nice-6106099", + "review": 0, + "should_download": 0, + "title": "Tushy erste Doppelpenetration f\u00fcr Natasha sch\u00f6n | xHamster", + "file_name": "Tushy erste Doppelpenetration f\u00fcr Natasha sch\u00f6n [6106099].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8eed03f1-b536-4451-ab44-699b51865857.mp4" + }, + { + "id": "8efc0364-2ebd-4195-b9c8-8938be499018", + "created_date": "2024-07-25 07:29:47.081447", + "last_modified_date": "2024-07-25 07:29:47.081447", + "version": 0, + "url": "https://ge.xhamster.com/videos/wife-in-a-bikini-gets-unprotected-creampie-and-facial-from-two-cocks-on-public-beach-xhIfw4p", + "review": 0, + "should_download": 0, + "title": "Ehefrau im Bikini bekommt ungesch\u00fctzten Creampie und Gesichtsbesamung von zwei Schw\u00e4nzen am \u00f6ffentlichen Strand | xHamster", + "file_name": "Ehefrau im Bikini bekommt ungesch\u00fctzten Creampie und Gesichtsbesamung von zwei Schw\u00e4nzen am \u00f6ffentlichen Strand [xhIfw4p].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8efc0364-2ebd-4195-b9c8-8938be499018.mp4" + }, + { + "id": "8f334780-aeb5-4ff3-a8c1-804a0ab93967", + "created_date": "2024-08-28 23:21:54.362667", + "last_modified_date": "2024-08-28 23:21:54.362667", + "version": 0, + "url": "https://ge.xhamster.com/videos/caught-fucking-by-their-parents-12844706", + "review": 0, + "should_download": 0, + "title": "Erwischt beim Ficken von ihren Eltern | xHamster", + "file_name": "Erwischt beim Ficken von ihren Eltern [12844706].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/8f334780-aeb5-4ff3-a8c1-804a0ab93967.mp4" + }, + { + "id": "8f82d5b7-7c90-4572-8b1b-848fa3e2c728", + "created_date": "2024-10-21 15:08:43.551517", + "last_modified_date": "2024-10-21 16:30:40.419000", + "version": 1, + "url": "https://ge.xhamster.com/videos/schoolgirls-scene-2-xhgz8nu", + "review": 0, + "should_download": 0, + "title": "Schulm\u00e4dchen-Szene 2 | xHamster", + "file_name": "Schulm\u00e4dchen-Szene 2 [xhgz8nu].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/8f82d5b7-7c90-4572-8b1b-848fa3e2c728.mp4" + }, + { + "id": "8facb33e-47ea-4372-b4f1-183cc741503f", + "created_date": "2024-07-25 07:29:45.079307", + "last_modified_date": "2024-07-25 07:29:45.079307", + "version": 0, + "url": "https://ge.xhamster.com/videos/slut-gets-her-asshole-drilled-in-the-garden-xhMbmOr", + "review": 0, + "should_download": 0, + "title": "Schlampe bekommt im Garten ihr Arschloch gebohrt | xHamster", + "file_name": "Schlampe bekommt im Garten ihr Arschloch gebohrt [xhMbmOr].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/8facb33e-47ea-4372-b4f1-183cc741503f.mp4" + }, + { + "id": "911de070-7a37-406a-a3bd-5ecf27a02e3e", + "created_date": "2024-07-25 07:29:47.124985", + "last_modified_date": "2024-07-25 07:29:47.124985", + "version": 0, + "url": "https://ge.xhamster.com/videos/2-stunden-buro-miezen-full-movie-xhf51Xk", + "review": 0, + "should_download": 0, + "title": "2 Stunden Buro Miezen Full Movie, Free HD Porn 1a | xHamster", + "file_name": "2 Stunden Buro Miezen (Full Movie) [xhf51Xk].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/911de070-7a37-406a-a3bd-5ecf27a02e3e.mp4" + }, + { + "id": "9138f276-692e-40af-8e24-d672df7e1118", + "created_date": "2024-07-25 07:29:45.813081", + "last_modified_date": "2024-07-25 07:29:45.813081", + "version": 0, + "url": "https://ge.xhamster.com/videos/great-threesome-3994235", + "review": 0, + "should_download": 0, + "title": "Toller Dreier | xHamster", + "file_name": "Toller Dreier [3994235].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9138f276-692e-40af-8e24-d672df7e1118.mp4" + }, + { + "id": "91ab647c-23a0-4021-95f3-fee357b26d12", + "created_date": "2024-07-25 07:29:44.395646", + "last_modified_date": "2024-07-25 07:29:44.395646", + "version": 0, + "url": "https://ge.xhamster.com/videos/models-auf-dem-prufstand-1999-german-full-video-dvd-rip-xhEAfHe", + "review": 0, + "should_download": 0, + "title": "Models Auf Dem Prufstand 1999 German Full Video Dvd Rip | xHamster", + "file_name": "Models auf dem Prufstand (1999, German, full video, DVD rip) [xhEAfHe].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/91ab647c-23a0-4021-95f3-fee357b26d12.mp4" + }, + { + "id": "91c77fcb-65fc-4312-95e4-d49db05511b4", + "created_date": "2024-07-25 07:29:46.659646", + "last_modified_date": "2024-07-25 07:29:46.659646", + "version": 0, + "url": "https://ge.xhamster.com/videos/special-order-higher-quality-xhXhSkP", + "review": 0, + "should_download": 0, + "title": "Sonderbestellung (h\u00f6here Qualit\u00e4t) | xHamster", + "file_name": "Sonderbestellung (h\u00f6here Qualit\u00e4t) [xhXhSkP].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/91c77fcb-65fc-4312-95e4-d49db05511b4.mp4" + }, + { + "id": "9265488d-e97e-4272-84f3-3310e5d88ef6", + "created_date": "2024-07-25 07:29:45.109037", + "last_modified_date": "2024-07-25 07:29:45.109037", + "version": 0, + "url": "https://ge.xhamster.com/videos/bratty-sis-messing-with-stepsis-and-my-cock-slips-in-s4-e2-9673701", + "review": 0, + "should_download": 0, + "title": "Brattige Schwester spielt mit Stiefschwester und mein Schwanz rutscht rein! s4: e2 | xHamster", + "file_name": "Brattige Schwester spielt mit Stiefschwester und mein Schwanz rutscht rein! s4\uff1a e2 [9673701].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9265488d-e97e-4272-84f3-3310e5d88ef6.mp4" + }, + { + "id": "9268a65a-d371-42aa-8990-29ea3e192c0c", + "created_date": "2024-07-25 07:29:45.101934", + "last_modified_date": "2024-07-25 07:29:45.101934", + "version": 0, + "url": "https://ge.xhamster.com/videos/backyard-summer-orgy-xh8Kuh2", + "review": 0, + "should_download": 0, + "title": "Hinterhof-Sommer-Orgie | xHamster", + "file_name": "Hinterhof-Sommer-Orgie [xh8Kuh2].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9268a65a-d371-42aa-8990-29ea3e192c0c.mp4" + }, + { + "id": "927117d5-0ad5-4c4e-9e2f-4c8ba8c3c9f7", + "created_date": "2024-07-25 07:29:45.930519", + "last_modified_date": "2024-07-25 07:29:45.930519", + "version": 0, + "url": "https://ge.xhamster.com/videos/die-versaute-nonne-full-movie-xhkdYAv", + "review": 0, + "should_download": 0, + "title": "Die Versaute Nonne Full Movie, Free Big Cock HD Porn 08 | xHamster", + "file_name": "Die Versaute Nonne (Full Movie) [xhkdYAv].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/927117d5-0ad5-4c4e-9e2f-4c8ba8c3c9f7.mp4" + }, + { + "id": "92732fa3-6030-4219-8bf0-5a07c7278925", + "created_date": "2024-12-29 23:53:27.961664", + "last_modified_date": "2024-12-29 23:53:27.961664", + "version": 0, + "url": "https://ge.xhamster.com/videos/erotic-adventure-1984-12373958", + "review": 0, + "should_download": 0, + "title": "Erotisches Abenteuer (1984) | xHamster", + "file_name": "Erotisches Abenteuer (1984) [12373958].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/92732fa3-6030-4219-8bf0-5a07c7278925.mp4" + }, + { + "id": "92a65421-b498-4e12-a73b-e41412933437", + "created_date": "2024-07-25 07:29:47.367911", + "last_modified_date": "2024-07-25 07:29:47.367911", + "version": 0, + "url": "https://ge.xhamster.com/videos/schoolgirls-geile-biester-auf-der-schulbank-1995-7720243", + "review": 0, + "should_download": 0, + "title": "Schoolgirls - Geile Biester Auf Der Schulbank 1995: Porn 8b | xHamster", + "file_name": "Schoolgirls - Geile Biester auf der Schulbank (1995) [7720243].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/92a65421-b498-4e12-a73b-e41412933437.mp4" + }, + { + "id": "92b09e8e-7946-42a5-aa0e-2fcc5cd44662", + "created_date": "2024-07-25 07:29:46.500238", + "last_modified_date": "2024-07-25 07:29:46.500238", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-best-friend-came-to-see-movies-but-i-fucked-her-xh4cWKj", + "review": 0, + "should_download": 0, + "title": "Meine beste freundin kam, um filme zu gucken, aber ich fickte sie | xHamster", + "file_name": "Meine beste freundin kam, um filme zu gucken, aber ich fickte sie [xh4cWKj].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/92b09e8e-7946-42a5-aa0e-2fcc5cd44662.mp4" + }, + { + "id": "92cc1c43-dfcc-400c-8566-e60254def2a6", + "created_date": "2024-07-25 07:29:46.015556", + "last_modified_date": "2024-07-25 07:29:46.015556", + "version": 0, + "url": "https://ge.xhamster.com/videos/vintage-hoppla-jetzt-komm-ich-xhmTm5V", + "review": 0, + "should_download": 0, + "title": "Vintage - Hoppla Jetzt Komm Ich, Free Porn cc | xHamster", + "file_name": "vintage - hoppla jetzt komm ich [xhmTm5V].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/92cc1c43-dfcc-400c-8566-e60254def2a6.mp4" + }, + { + "id": "93b4b18b-4dd3-4333-adf5-f5ece6fa3a65", + "created_date": "2024-07-25 07:29:46.311061", + "last_modified_date": "2024-07-25 07:29:46.311061", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-stepdad-and-his-stepbrother-fuck-my-holes-xh6z2tG", + "review": 0, + "should_download": 0, + "title": "Mein Stiefvater und sein Stiefbruder ficken meine L\u00f6cher | xHamster", + "file_name": "Mein Stiefvater und sein Stiefbruder ficken meine L\u00f6cher [xh6z2tG].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/93b4b18b-4dd3-4333-adf5-f5ece6fa3a65.mp4" + }, + { + "id": "93de10fa-b6e5-4e46-8196-0b84db62a937", + "created_date": "2024-07-25 07:29:44.247065", + "last_modified_date": "2024-07-25 07:29:44.247065", + "version": 0, + "url": "https://ge.xhamster.com/videos/bratty-sis-my-cock-slips-in-sisters-pussy-and-she-loves-it-8519881", + "review": 0, + "should_download": 0, + "title": "Bratty Sis - mein Schwanz rutscht in die Muschi der Schwester und sie liebt es | xHamster", + "file_name": "Bratty Sis - mein Schwanz rutscht in die Muschi der Schwester und sie liebt es [8519881].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/93de10fa-b6e5-4e46-8196-0b84db62a937.mp4" + }, + { + "id": "94001fb9-162e-4e55-a26f-71e44db34a19", + "created_date": "2024-09-11 10:23:29.176180", + "last_modified_date": "2024-10-21 16:30:45.132000", + "version": 1, + "url": "https://ge.xhamster.com/videos/stranger-cocks-caught-jerking-off-on-the-beach-and-juiced-xhihRQi", + "review": 0, + "should_download": 0, + "title": "Sch\u00f6nste Fremdschw\u00e4nze am Beach beim Wichsen erwischt und entsaftet | xHamster", + "file_name": "Sch\u00f6nste Fremdschw\u00e4nze am Beach beim Wichsen erwischt und entsaftet [xhihRQi].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/94001fb9-162e-4e55-a26f-71e44db34a19.mp4" + }, + { + "id": "9408796f-acd7-467b-924c-f605823326e1", + "created_date": "2024-07-25 07:29:44.857332", + "last_modified_date": "2024-07-25 07:29:44.857332", + "version": 0, + "url": "https://ge.xhamster.com/videos/american-taboo-3-xhHBJkZ", + "review": 0, + "should_download": 0, + "title": "Amerikanisches Tabu 3 | xHamster", + "file_name": "Amerikanisches Tabu 3 [xhHBJkZ].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9408796f-acd7-467b-924c-f605823326e1.mp4" + }, + { + "id": "944b6f42-4169-44fd-96cc-d10d7e5053cc", + "created_date": "2024-07-25 07:29:45.536342", + "last_modified_date": "2024-07-25 07:29:45.536342", + "version": 0, + "url": "https://ge.xhamster.com/videos/bi-in-der-schule-3858838", + "review": 0, + "should_download": 0, + "title": "Bi in Der Schule: Free Threesome Porn Video de | xHamster", + "file_name": "Bi in der Schule [3858838].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/944b6f42-4169-44fd-96cc-d10d7e5053cc.mp4" + }, + { + "id": "94857778-18c2-4061-a1c8-64a2fe923fd8", + "created_date": "2024-12-29 23:53:27.951571", + "last_modified_date": "2024-12-29 23:53:27.951571", + "version": 0, + "url": "https://ge.xhamster.com/videos/your-perverted-stepbrother-has-a-boner-xhrUsA8", + "review": 0, + "should_download": 0, + "title": "Dein perverser Stiefbruder hat eine Latte! | xHamster", + "file_name": "Dein perverser Stiefbruder hat eine Latte! [xhrUsA8].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/94857778-18c2-4061-a1c8-64a2fe923fd8.mp4" + }, + { + "id": "9489ef3c-e56e-4a54-8843-688d7e82aea9", + "created_date": "2024-12-29 23:53:27.933552", + "last_modified_date": "2024-12-29 23:53:27.933552", + "version": 0, + "url": "https://ge.xhamster.com/videos/bratty-sis-horny-bro-slips-cock-into-besties-teen-pussy-10303594", + "review": 0, + "should_download": 0, + "title": "Bratty Sis - ein geiler Bro rutscht Schwanz in die Teen-Muschi von Besties | xHamster", + "file_name": "Bratty Sis - ein geiler Bro rutscht Schwanz in die Teen-Muschi von Besties [10303594].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/9489ef3c-e56e-4a54-8843-688d7e82aea9.mp4" + }, + { + "id": "94a28ae0-a89f-453e-96c2-af2a5f54b61e", + "created_date": "2024-07-25 07:29:45.996882", + "last_modified_date": "2024-07-25 07:29:45.996882", + "version": 0, + "url": "https://ge.xhamster.com/videos/unerwartete-sexuelle-gelegenheiten-full-movie-xhzWrt0", + "review": 0, + "should_download": 0, + "title": "Unerwartete Sexuelle Gelegenheiten Full Movie: Free Porn 27 | xHamster", + "file_name": "Unerwartete sexuelle Gelegenheiten (Full Movie) [xhzWrt0].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/94a28ae0-a89f-453e-96c2-af2a5f54b61e.mp4" + }, + { + "id": "94a3b7ab-0862-4422-a326-46af2f1cf9a6", + "created_date": "2024-07-25 07:29:45.389999", + "last_modified_date": "2024-07-25 07:29:45.389999", + "version": 0, + "url": "https://ge.xhamster.com/videos/18-year-old-girl-gets-her-ass-deflowered-in-front-of-her-friend-xhkvWed", + "review": 0, + "should_download": 0, + "title": "18-J\u00e4hrige l\u00e4sst sich vor ihrer Freundin den Arsch entjungfern! | xHamster", + "file_name": "18-J\u00e4hrige l\u00e4sst sich vor ihrer Freundin den Arsch entjungfern! [xhkvWed].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/94a3b7ab-0862-4422-a326-46af2f1cf9a6.mp4" + }, + { + "id": "94b76c97-140c-4cf0-b256-c6ba9a022aee", + "created_date": "2024-07-25 07:29:47.532780", + "last_modified_date": "2024-07-25 07:29:47.532780", + "version": 0, + "url": "https://ge.xhamster.com/videos/american-college-xxx-vol-9-original-version-in-hd-xh81OaU", + "review": 0, + "should_download": 0, + "title": "American College xxx - vol (9) - (Originalversion in hd) | xHamster", + "file_name": "American College xxx - vol (9) - (Originalversion in hd) [xh81OaU].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/94b76c97-140c-4cf0-b256-c6ba9a022aee.mp4" + }, + { + "id": "94d4d56c-2a78-4e65-b7c8-3eb499f2329d", + "created_date": "2024-07-25 07:29:44.547159", + "last_modified_date": "2024-07-25 07:29:44.547159", + "version": 0, + "url": "https://ge.xhamster.com/videos/pure-orgy-1979-12159580", + "review": 0, + "should_download": 0, + "title": "Pure Orgie (1979) | xHamster", + "file_name": "Pure Orgie (1979) [12159580].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/94d4d56c-2a78-4e65-b7c8-3eb499f2329d.mp4" + }, + { + "id": "94f2fe48-6014-4d35-ba20-d8f5e736f7ee", + "created_date": "2024-07-25 07:29:45.571728", + "last_modified_date": "2024-07-25 07:29:45.571728", + "version": 0, + "url": "https://ge.xhamster.com/videos/die-munteren-sexspiele-unserer-nachbarn-1978-softcore-1630973", + "review": 0, + "should_download": 0, + "title": "Die Munteren Sexspiele Unserer Nachbarn 1978 Softcore | xHamster", + "file_name": "Die Munteren Sexspiele Unserer Nachbarn (1978) Softcore [1630973].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/94f2fe48-6014-4d35-ba20-d8f5e736f7ee.mp4" + }, + { + "id": "951cb63a-85f6-4c6b-a8c5-ae389ae4518d", + "created_date": "2024-12-29 23:53:27.896983", + "last_modified_date": "2024-12-29 23:53:27.896983", + "version": 0, + "url": "https://ge.xhamster.com/videos/hot-sauna-threesome-xhu1nDi", + "review": 0, + "should_download": 0, + "title": "Hei\u00dfer Sauna-Dreier | xHamster", + "file_name": "Hei\u00dfer Sauna-Dreier [xhu1nDi].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/951cb63a-85f6-4c6b-a8c5-ae389ae4518d.mp4" + }, + { + "id": "9527957a-ab3b-4273-a399-847a322d3532", + "created_date": "2024-07-25 07:29:48.021128", + "last_modified_date": "2024-07-25 07:29:48.021128", + "version": 0, + "url": "https://ge.xhamster.com/videos/summer-heat-1979-15008833", + "review": 0, + "should_download": 0, + "title": "Summer Heat (1979) | xHamster", + "file_name": "Summer Heat (1979) [15008833].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9527957a-ab3b-4273-a399-847a322d3532.mp4" + }, + { + "id": "9536377d-d3f6-4dee-ad12-b00666569775", + "created_date": "2024-07-25 07:29:46.489069", + "last_modified_date": "2024-07-25 07:29:46.489069", + "version": 0, + "url": "https://ge.xhamster.com/videos/i-saw-my-stepmom-masturbating-s15-e6-xhWZQgd", + "review": 0, + "should_download": 0, + "title": "Ich habe meine stiefmutter masturbiert - s15: e6 | xHamster", + "file_name": "Ich habe meine stiefmutter masturbiert - s15\uff1a e6 [xhWZQgd].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9536377d-d3f6-4dee-ad12-b00666569775.mp4" + }, + { + "id": "9556c2b8-a2b2-42a9-83ca-0f1f6d8bbba7", + "created_date": "2024-07-25 07:29:45.128161", + "last_modified_date": "2024-07-25 07:29:45.128161", + "version": 0, + "url": "https://ge.xhamster.com/videos/vintage-anal-full-movie-01-little-french-maid-a85-xhxZ8SI", + "review": 0, + "should_download": 0, + "title": "Retro anal, kompletter Film 01 (kleines franz\u00f6sisches Zimmerm\u00e4dchen) - a85 | xHamster", + "file_name": "Retro anal, kompletter Film 01 (kleines franz\u00f6sisches Zimmerm\u00e4dchen) - a85 [xhxZ8SI].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/9556c2b8-a2b2-42a9-83ca-0f1f6d8bbba7.mp4" + }, + { + "id": "95702616-a5a0-411d-a82f-59cd45bff466", + "created_date": "2024-07-25 07:29:47.676118", + "last_modified_date": "2024-07-25 07:29:47.676118", + "version": 0, + "url": "https://ge.xhamster.com/videos/games-with-stepsis-xhbMCjr", + "review": 0, + "should_download": 0, + "title": "Spiele mit Stiefschwester | xHamster", + "file_name": "Spiele mit Stiefschwester [xhbMCjr].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/95702616-a5a0-411d-a82f-59cd45bff466.mp4" + }, + { + "id": "957b5397-fc0e-4f50-a6f9-fe71f9388462", + "created_date": "2024-07-25 07:29:44.238523", + "last_modified_date": "2024-07-25 07:29:44.238523", + "version": 0, + "url": "https://ge.xhamster.com/videos/kinky-family-lacy-lennon-my-stepsis-took-my-virginity-11178868", + "review": 0, + "should_download": 0, + "title": "Versaute Familie - Lacy Lennon - meine Stiefschwester hat meine Jungfr\u00e4ulichkeit genommen | xHamster", + "file_name": "Versaute Familie - Lacy Lennon - meine Stiefschwester hat meine Jungfr\u00e4ulichkeit genommen [11178868].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/957b5397-fc0e-4f50-a6f9-fe71f9388462.mp4" + }, + { + "id": "958ec084-5033-441d-af65-9e10ed9729f1", + "created_date": "2024-07-25 07:29:46.403732", + "last_modified_date": "2024-07-25 07:29:46.403732", + "version": 0, + "url": "https://ge.xhamster.com/videos/you-should-spend-more-time-outdoors-8-6801304", + "review": 0, + "should_download": 0, + "title": "Sie sollten mehr Zeit im Freien verbringen # 8 | xHamster", + "file_name": "Sie sollten mehr Zeit im Freien verbringen # 8 [6801304].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/958ec084-5033-441d-af65-9e10ed9729f1.mp4" + }, + { + "id": "95d3690f-3acb-4b8a-9aa3-e8c9993bcddd", + "created_date": "2024-07-25 07:29:46.469630", + "last_modified_date": "2024-07-25 07:29:46.469630", + "version": 0, + "url": "https://ge.xhamster.com/videos/bratty-sis-being-extra-nice-to-her-step-brother-s7-e9-10922908", + "review": 0, + "should_download": 0, + "title": "Bratty sis - besonders nett zu ihrem Stiefbruder s7: e9 sein | xHamster", + "file_name": "Bratty sis - besonders nett zu ihrem Stiefbruder s7\uff1a e9 sein [10922908].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/95d3690f-3acb-4b8a-9aa3-e8c9993bcddd.mp4" + }, + { + "id": "95f37065-f536-4e60-bfb5-1411fc3f1599", + "created_date": "2024-07-25 07:29:44.499198", + "last_modified_date": "2024-07-25 07:29:44.499198", + "version": 0, + "url": "https://ge.xhamster.com/videos/pretty-woman-asked-to-smear-her-back-with-cream-xhERPxV", + "review": 0, + "should_download": 0, + "title": "H\u00fcbsche frau bat, sie mit lotion zu reiben | xHamster", + "file_name": "H\u00fcbsche frau bat, sie mit lotion zu reiben [xhERPxV].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/95f37065-f536-4e60-bfb5-1411fc3f1599.mp4" + }, + { + "id": "95f89afd-d77a-455b-b5a9-9f4d9c6cb5e0", + "created_date": "2024-07-25 07:29:47.762798", + "last_modified_date": "2024-07-25 07:29:47.762798", + "version": 0, + "url": "https://ge.xhamster.com/videos/swapdaughter-molly-little-shows-milf-katrina-colt-the-perks-of-being-freeuse-s7-e5-xhemlnp", + "review": 0, + "should_download": 0, + "title": "Swapdaughter Molly Little zeigt MILF Katrina Colt die perks, \"Freeuse\" zu sein - S7: E5 | xHamster", + "file_name": "Swapdaughter Molly Little zeigt MILF Katrina Colt die perks, \uff02Freeuse\uff02 zu sein - S7\uff1a E5 [xhemlnp].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/95f89afd-d77a-455b-b5a9-9f4d9c6cb5e0.mp4" + }, + { + "id": "9623298e-4b60-4870-a1eb-bec06fb97e02", + "created_date": "2024-09-24 08:11:38.999255", + "last_modified_date": "2024-10-21 16:30:59.458000", + "version": 1, + "url": "https://ge.xhamster.com/videos/pure-taboo-scheming-boyfriend-wants-to-see-his-gfs-stepmom-sarah-vandella-make-her-orgasm-xh4dYTP", + "review": 0, + "should_download": 0, + "title": "Pure Tabu, intriganter Freund will sehen, wie die Stiefmutter seiner Freundin Sarah Vandella ihren Orgasmus macht | xHamster", + "file_name": "Pure Tabu, intriganter Freund will sehen, wie die Stiefmutter seiner Freundin Sarah Vandella ihren Orgasmus macht [xh4dYTP].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/9623298e-4b60-4870-a1eb-bec06fb97e02.mp4" + }, + { + "id": "966848c9-20ec-4fb3-8902-8e5523fcd0d6", + "created_date": "2024-07-25 07:29:46.926120", + "last_modified_date": "2024-07-25 07:29:46.926120", + "version": 0, + "url": "https://ge.xhamster.com/videos/familie-brunzbichler-3-3156748", + "review": 0, + "should_download": 0, + "title": "Familie Brunzbichler 3, Free Hardcore Porn 52 | xHamster", + "file_name": "Familie Brunzbichler 3 [3156748].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/966848c9-20ec-4fb3-8902-8e5523fcd0d6.mp4" + }, + { + "id": "96a63276-f917-4bea-ad28-fc1f95ef3eb1", + "created_date": "2024-07-25 07:29:46.053259", + "last_modified_date": "2024-07-25 07:29:46.053259", + "version": 0, + "url": "https://ge.xhamster.com/videos/a-super-busty-german-brunette-gets-her-muff-hammered-outdoors-xhBBjTp", + "review": 0, + "should_download": 0, + "title": "Eine super vollbusige deutsche Br\u00fcnette bekommt drau\u00dfen ihre Muschi geh\u00e4mmert | xHamster", + "file_name": "Eine super vollbusige deutsche Br\u00fcnette bekommt drau\u00dfen ihre Muschi geh\u00e4mmert [xhBBjTp].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/96a63276-f917-4bea-ad28-fc1f95ef3eb1.mp4" + }, + { + "id": "970f6be9-f5dc-4b0f-b6cd-2348c8e77661", + "created_date": "2024-07-25 07:29:46.324252", + "last_modified_date": "2024-07-25 07:29:46.324252", + "version": 0, + "url": "https://ge.xhamster.com/videos/pov-of-the-wife-blowing-me-in-the-creek-xhLwTKD", + "review": 0, + "should_download": 0, + "title": "POV der ehefrau bl\u00e4st mich im bach | xHamster", + "file_name": "POV der ehefrau bl\u00e4st mich im bach [xhLwTKD].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/970f6be9-f5dc-4b0f-b6cd-2348c8e77661.mp4" + }, + { + "id": "971d23d0-72e9-4173-ad70-aa01394cff8b", + "created_date": "2024-07-25 07:29:47.175568", + "last_modified_date": "2024-07-25 07:29:47.175568", + "version": 0, + "url": "https://ge.xhamster.com/videos/hot-teacher-tricks-students-into-threeway-fuck-7032708", + "review": 0, + "should_download": 0, + "title": "Hei\u00dfe Lehrerin trickst Sch\u00fcler in Dreier-Fick aus | xHamster", + "file_name": "Hei\u00dfe Lehrerin trickst Sch\u00fcler in Dreier-Fick aus [7032708].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/971d23d0-72e9-4173-ad70-aa01394cff8b.mp4" + }, + { + "id": "97512386-f969-4d0b-a0ed-c9cce7af3b6a", + "created_date": "2024-07-25 07:29:44.729616", + "last_modified_date": "2024-07-25 07:29:44.729616", + "version": 0, + "url": "https://ge.xhamster.com/videos/secret-vacation-xhFh3Nq", + "review": 0, + "should_download": 0, + "title": "Geheime Ferien | xHamster", + "file_name": "Geheime Ferien [xhFh3Nq].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/97512386-f969-4d0b-a0ed-c9cce7af3b6a.mp4" + }, + { + "id": "9777c0c1-dec4-4d43-a707-8ced8cf3d5e7", + "created_date": "2024-07-25 07:29:44.314917", + "last_modified_date": "2024-07-25 07:29:44.314917", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-friend-was-coming-to-the-solarium-at-the-club-and-ended-up-having-sex-with-me-and-my-friends-at-the-pool-xhKLBoG", + "review": 0, + "should_download": 0, + "title": "Mein Freund kam ins Solarium des Clubs und hatte am Ende Sex mit mir und meinen Freunden am Pool | xHamster", + "file_name": "Mein Freund kam ins Solarium des Clubs und hatte am Ende Sex mit mir und meinen Freunden am Pool [xhKLBoG].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/9777c0c1-dec4-4d43-a707-8ced8cf3d5e7.mp4" + }, + { + "id": "97bce4aa-9306-4fd5-b3a5-eafaf8daedd8", + "created_date": "2024-07-25 07:29:46.416644", + "last_modified_date": "2024-07-25 07:29:46.416644", + "version": 0, + "url": "https://ge.xhamster.com/videos/step-brother-cum-twice-with-his-new-step-sister-xhWUdlD", + "review": 0, + "should_download": 0, + "title": "Stiefbruder kommt zweimal mit seiner neuen Stiefschwester | xHamster", + "file_name": "Stiefbruder kommt zweimal mit seiner neuen Stiefschwester [xhWUdlD].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/97bce4aa-9306-4fd5-b3a5-eafaf8daedd8.mp4" + }, + { + "id": "97c2da74-f79a-4af6-a20c-7bf7a74dfa62", + "created_date": "2024-07-25 07:29:46.281847", + "last_modified_date": "2024-07-25 07:29:46.281847", + "version": 0, + "url": "https://ge.xhamster.com/videos/nachsitzen-nr-1-full-movie-xhGgGzw", + "review": 0, + "should_download": 0, + "title": "Nachsitzen Nr 1 Full Movie, Free Big Cock Porn ff | xHamster", + "file_name": "Nachsitzen Nr.1 (Full Movie) [xhGgGzw].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/97c2da74-f79a-4af6-a20c-7bf7a74dfa62.mp4" + }, + { + "id": "97c5c7e5-77a3-4822-a47f-a9f9437202fd", + "created_date": "2024-07-25 07:29:44.566165", + "last_modified_date": "2024-07-25 07:29:44.566165", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsis-says-your-sperm-are-probably-so-little-you-wouldnt-even-have-a-chance-of-getting-me-pregnant-xhN7r0m", + "review": 0, + "should_download": 0, + "title": "Stiefschwester sagt, dein Sperma ist wahrscheinlich so klein, dass du keine Chance h\u00e4ttest, mich schwanger zu machen | xHamster", + "file_name": "Stiefschwester sagt, dein Sperma ist wahrscheinlich so klein, dass du keine Chance h\u00e4ttest, mich schwanger zu machen [xhN7r0m].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/97c5c7e5-77a3-4822-a47f-a9f9437202fd.mp4" + }, + { + "id": "97dbcc38-cb27-4efb-ad92-02871687a1e2", + "created_date": "2024-07-25 07:29:44.251056", + "last_modified_date": "2024-07-25 07:29:44.251056", + "version": 0, + "url": "https://ge.xhamster.com/videos/teen-vanilla-skye-gets-horny-in-the-pool-and-loves-contorting-to-suck-dick-xhBFNAu", + "review": 0, + "should_download": 0, + "title": "Teenie Vanilla Skye wird im Pool geil und liebt es, sich zu lutschen, um Schwanz zu lutschen | xHamster", + "file_name": "Teenie Vanilla Skye wird im Pool geil und liebt es, sich zu lutschen, um Schwanz zu lutschen [xhBFNAu].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/97dbcc38-cb27-4efb-ad92-02871687a1e2.mp4" + }, + { + "id": "985687ee-d6c7-4748-a610-e75f75029673", + "created_date": "2024-07-25 07:29:44.590039", + "last_modified_date": "2024-07-25 07:29:44.590039", + "version": 0, + "url": "https://ge.xhamster.com/videos/at-the-office-come-fuck-my-wet-horny-pussy-and-splash-me-with-your-juice-xhhJ3Da", + "review": 0, + "should_download": 0, + "title": "Im B\u00fcro: Komm, fick meine nasse, geile Muschi und spritz mich mit deinem Saft | xHamster", + "file_name": "Im B\u00fcro\uff1a Komm, fick meine nasse, geile Muschi und spritz mich mit deinem Saft [xhhJ3Da].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/985687ee-d6c7-4748-a610-e75f75029673.mp4" + }, + { + "id": "9876e83a-e7b8-45d3-a200-be16a1a8c4a1", + "created_date": "2024-07-25 07:29:45.529325", + "last_modified_date": "2024-07-25 07:29:45.529325", + "version": 0, + "url": "https://ge.xhamster.com/videos/german-amateur-vintage-complete-film-b-r-11312553", + "review": 0, + "should_download": 0, + "title": "Deutscher Amateur-Retro - kompletter Film -b $ r | xHamster", + "file_name": "Deutscher Amateur-Retro - kompletter Film -b $ r [11312553].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9876e83a-e7b8-45d3-a200-be16a1a8c4a1.mp4" + }, + { + "id": "9882123d-539a-4a03-9c59-d3a98ecbcc39", + "created_date": "2024-07-25 07:29:45.567222", + "last_modified_date": "2024-07-25 07:29:45.567222", + "version": 0, + "url": "https://ge.xhamster.com/videos/lexi-luna-plays-with-chloe-surreal-before-sharing-her-bfs-dick-with-her-stepdaughter-leana-lovings-brazzers-xhC23iG", + "review": 0, + "should_download": 0, + "title": "Lexi Luna spielt mit Chloe Surreal, bevor sie den Schwanz ihres Freundes mit ihrer Stieftochter Leana Lovings - Brazzers - teilt | xHamster", + "file_name": "Lexi Luna spielt mit Chloe Surreal, bevor sie den Schwanz ihres Freundes mit ihrer Stieftochter Leana Lovings - Brazzers - teilt [xhC23iG].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9882123d-539a-4a03-9c59-d3a98ecbcc39.mp4" + }, + { + "id": "98859d43-9ce0-4a7f-9e36-2a975597cbab", + "created_date": "2024-07-25 07:29:44.752928", + "last_modified_date": "2024-07-25 07:29:44.752928", + "version": 0, + "url": "https://ge.xhamster.com/videos/after-a-night-out-at-the-disco-we-hooked-up-with-this-big-ass-xhFuTro", + "review": 0, + "should_download": 0, + "title": "Nach einer nacht in der disco haben wir es mit diesem dicken arsch gefickt | xHamster", + "file_name": "Nach einer nacht in der disco haben wir es mit diesem dicken arsch gefickt [xhFuTro].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/98859d43-9ce0-4a7f-9e36-2a975597cbab.mp4" + }, + { + "id": "98c8dd69-26f4-4317-9a35-ed6b43e9bc5e", + "created_date": "2024-09-24 08:11:39.006580", + "last_modified_date": "2024-10-21 16:31:04.988000", + "version": 1, + "url": "https://ge.xhamster.com/videos/sister-teach-brother-a-lesson-for-peeping-12908263", + "review": 0, + "should_download": 0, + "title": "Schwester lehrt Bruder eine Lektion f\u00fcr das Gucken | xHamster", + "file_name": "Schwester lehrt Bruder eine Lektion f\u00fcr das Gucken [12908263].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/98c8dd69-26f4-4317-9a35-ed6b43e9bc5e.mp4" + }, + { + "id": "98c958ea-ff30-4ca8-81c6-f92f3bf64b28", + "created_date": "2024-12-29 23:53:27.936731", + "last_modified_date": "2024-12-29 23:53:27.936731", + "version": 0, + "url": "https://ge.xhamster.com/videos/step-brother-step-sister-share-a-hotel-room-rhaya-shyne-teamskeet-classics-xhe9fqf", + "review": 0, + "should_download": 0, + "title": "Stiefbruder und Stiefschwester teilen sich ein Hotelzimmer - rhaya shyne - Klassiker im Teamskeet | xHamster", + "file_name": "Stiefbruder und Stiefschwester teilen sich ein Hotelzimmer - rhaya shyne - Klassiker im Teamskeet [xhe9fqf].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/98c958ea-ff30-4ca8-81c6-f92f3bf64b28.mp4" + }, + { + "id": "9928b4a6-130d-4c64-930d-a5b1c8b60afc", + "created_date": "2024-07-25 07:29:46.234992", + "last_modified_date": "2024-07-25 07:29:46.234992", + "version": 0, + "url": "https://ge.xhamster.com/videos/secretary-fucks-boss-waiting-for-her-husband-to-pick-her-up-xhO6alz", + "review": 0, + "should_download": 0, + "title": "Sekret\u00e4rin fickt Chef und wartet darauf, dass ihr Ehemann sie abholt | xHamster", + "file_name": "Sekret\u00e4rin fickt Chef und wartet darauf, dass ihr Ehemann sie abholt [xhO6alz].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9928b4a6-130d-4c64-930d-a5b1c8b60afc.mp4" + }, + { + "id": "998424c2-3a19-486c-a617-2cb19c34975e", + "created_date": "2024-07-25 07:29:47.254496", + "last_modified_date": "2024-07-25 07:29:47.254496", + "version": 0, + "url": "https://ge.xhamster.com/videos/dont-worry-he-is-my-stepdaddy-vol-10-xhia72d", + "review": 0, + "should_download": 0, + "title": "Keine Sorge, er ist mein Stiefvater !!! - Vol # 10 | xHamster", + "file_name": "Keine Sorge, er ist mein Stiefvater !!! - Vol # 10 [xhia72d].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/998424c2-3a19-486c-a617-2cb19c34975e.mp4" + }, + { + "id": "99b87cb8-2f13-4b3e-b33d-116938b28eaa", + "created_date": "2024-07-25 07:29:44.764263", + "last_modified_date": "2024-07-25 07:29:44.764263", + "version": 0, + "url": "https://ge.xhamster.com/videos/die-beichte-der-josefinemutzenbacher-1979-5841900", + "review": 0, + "should_download": 0, + "title": "Die Beichte Der Josefinemutzenbacher 1979: Free Porn c7 | xHamster", + "file_name": "Die Beichte der JosefineMutzenbacher 1979 [5841900].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/99b87cb8-2f13-4b3e-b33d-116938b28eaa.mp4" + }, + { + "id": "99cfeb34-dd02-4b95-9080-903c31a65a54", + "created_date": "2024-07-25 07:29:44.515484", + "last_modified_date": "2024-07-25 07:29:44.515484", + "version": 0, + "url": "https://ge.xhamster.com/videos/versaute-familie-xhaTScH", + "review": 0, + "should_download": 0, + "title": "Versaute Familie: Free European Porn Video aa | xHamster", + "file_name": "Versaute Familie [xhaTScH].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/99cfeb34-dd02-4b95-9080-903c31a65a54.mp4" + }, + { + "id": "99e6bf98-7d7d-4af7-a43f-724a01723b66", + "created_date": "2024-07-25 07:29:45.174370", + "last_modified_date": "2024-07-25 07:29:45.174370", + "version": 0, + "url": "https://ge.xhamster.com/videos/ein-total-versautes-buro-3261734", + "review": 0, + "should_download": 0, + "title": "Ein Total Versautes Buro, Free Retro Porn 8f | xHamster", + "file_name": "Ein Total Versautes Buro [3261734].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/99e6bf98-7d7d-4af7-a43f-724a01723b66.mp4" + }, + { + "id": "99fb9b0d-c63e-4433-be56-5030ebe25ac2", + "created_date": "2024-07-25 07:29:47.167182", + "last_modified_date": "2024-07-25 07:29:47.167182", + "version": 0, + "url": "https://ge.xhamster.com/videos/neighborhood-doctor-punishment-3661901", + "review": 0, + "should_download": 0, + "title": "Nachbarschaftsarzt-Bestrafung | xHamster", + "file_name": "Nachbarschaftsarzt-Bestrafung [3661901].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/99fb9b0d-c63e-4433-be56-5030ebe25ac2.mp4" + }, + { + "id": "9a002058-a3b7-411a-88c2-00c38f136211", + "created_date": "2024-07-25 07:29:47.696579", + "last_modified_date": "2024-07-25 07:29:47.696579", + "version": 0, + "url": "https://ge.xhamster.com/videos/junge-knospen-budding-beauties-1991-xhCVAoW", + "review": 0, + "should_download": 0, + "title": "Junge Knospen - Budding Beauties 1991, Porn f1 | xHamster", + "file_name": "Junge Knospen - Budding Beauties 1991 [xhCVAoW].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9a002058-a3b7-411a-88c2-00c38f136211.mp4" + }, + { + "id": "9a02e41f-6def-406b-9bf5-f35dd154d7af", + "created_date": "2024-07-25 07:29:47.635929", + "last_modified_date": "2024-07-25 07:29:47.635929", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsiblings-stepbro-helps-sis-shave-and-licked-in-her-pus-9478938", + "review": 0, + "should_download": 0, + "title": "Stiefschwester - Stiefbruder hilft ihrer Schwester, sich zu rasieren und ihren Eiter zu lecken | xHamster", + "file_name": "Stiefschwester - Stiefbruder hilft ihrer Schwester, sich zu rasieren und ihren Eiter zu lecken [9478938].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9a02e41f-6def-406b-9bf5-f35dd154d7af.mp4" + }, + { + "id": "9abf8d88-1b41-47d5-a88f-a4c10ecd8523", + "created_date": "2024-07-25 07:29:45.858444", + "last_modified_date": "2024-07-25 07:29:45.858444", + "version": 0, + "url": "https://ge.xhamster.com/videos/marilyn-jess-1-german-vintage-compilation-70s-80s-2092799", + "review": 0, + "should_download": 0, + "title": "Marilyn Jess 1 deutsche Retro-Zusammenstellung der 70er und 80er Jahre | xHamster", + "file_name": "Marilyn Jess 1 deutsche Retro-Zusammenstellung der 70er und 80er Jahre [2092799].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9abf8d88-1b41-47d5-a88f-a4c10ecd8523.mp4" + }, + { + "id": "9b64eeff-4864-4a9a-8b29-1c047ae466f0", + "created_date": "2024-07-25 07:29:47.809360", + "last_modified_date": "2024-07-25 07:29:47.809360", + "version": 0, + "url": "https://ge.xhamster.com/videos/auf-hoher-see-full-german-movie-xhxTkgk", + "review": 0, + "should_download": 0, + "title": "Auf Hoher See- Full German Movie, Free Porn 9d | xHamster", + "file_name": "Auf Hoher See- full german movie [xhxTkgk].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9b64eeff-4864-4a9a-8b29-1c047ae466f0.mp4" + }, + { + "id": "9b7a30d9-b0ca-489b-83ce-8c955eababe3", + "created_date": "2024-07-25 07:29:44.617073", + "last_modified_date": "2024-07-25 07:29:44.617073", + "version": 0, + "url": "https://ge.xhamster.com/videos/sperma-schluckspechte-1990-xhdN6uc", + "review": 0, + "should_download": 0, + "title": "Sperma Schluckspechte 1990, Free European Porn bf | xHamster", + "file_name": "Sperma Schluckspechte (1990) [xhdN6uc].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/9b7a30d9-b0ca-489b-83ce-8c955eababe3.mp4" + }, + { + "id": "9b8058ec-9305-46b6-bc6f-6b928a6f0212", + "created_date": "2024-07-25 07:29:46.866901", + "last_modified_date": "2024-07-25 07:29:46.866901", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-friends-hot-mom-is-alyssa-bruce-xhhEp44", + "review": 0, + "should_download": 0, + "title": "Die hei\u00dfe mutter meines freundes ist Alyssa bruce | xHamster", + "file_name": "Die hei\u00dfe mutter meines freundes ist Alyssa bruce [xhhEp44].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9b8058ec-9305-46b6-bc6f-6b928a6f0212.mp4" + }, + { + "id": "9bec3afd-8011-4289-813e-e2a538044c2b", + "created_date": "2024-07-25 07:29:44.772331", + "last_modified_date": "2024-07-25 07:29:44.772331", + "version": 0, + "url": "https://ge.xhamster.com/videos/sextsunami-84-10676705", + "review": 0, + "should_download": 0, + "title": "Sextsunami 84: Free HD Porn Video 11 | xHamster", + "file_name": "sextsunami 84 [10676705].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9bec3afd-8011-4289-813e-e2a538044c2b.mp4" + }, + { + "id": "9c011916-bc0d-41bf-ba12-651bb4f657fa", + "created_date": "2024-07-25 07:29:45.633085", + "last_modified_date": "2024-07-25 07:29:45.633085", + "version": 0, + "url": "https://ge.xhamster.com/videos/wild-holidays-full-movie-xhtJfKB", + "review": 0, + "should_download": 0, + "title": "Wilde Feiertage (kompletter Film) | xHamster", + "file_name": "Wilde Feiertage (kompletter Film) [xhtJfKB].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9c011916-bc0d-41bf-ba12-651bb4f657fa.mp4" + }, + { + "id": "9c1b1beb-26ff-4ad0-b5bb-59231a0556ca", + "created_date": "2024-07-25 07:29:47.600783", + "last_modified_date": "2024-07-25 07:29:47.600783", + "version": 0, + "url": "https://ge.xhamster.com/videos/evhb-14847939", + "review": 0, + "should_download": 0, + "title": "Evhb | xHamster", + "file_name": "Evhb [14847939].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9c1b1beb-26ff-4ad0-b5bb-59231a0556ca.mp4" + }, + { + "id": "9cf124fa-323b-475b-91f8-b3c698696f1d", + "created_date": "2024-07-25 07:29:44.519056", + "last_modified_date": "2024-07-25 07:29:44.519056", + "version": 0, + "url": "https://ge.xhamster.com/videos/hot-wife-cory-chase-shared-and-dpd-after-gym-workout-xh0nbi1", + "review": 0, + "should_download": 0, + "title": "Hei\u00dfe Ehefrau Cory Chase geteilt und nach dem Fitnesstraining doppelpenetriert | xHamster", + "file_name": "Hei\u00dfe Ehefrau Cory Chase geteilt und nach dem Fitnesstraining doppelpenetriert [xh0nbi1].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9cf124fa-323b-475b-91f8-b3c698696f1d.mp4" + }, + { + "id": "9d60df03-6dfa-4683-a624-10fe6162dcae", + "created_date": "2024-07-25 07:29:47.752365", + "last_modified_date": "2024-09-06 09:41:31.078000", + "version": 1, + "url": "https://ge.xhamster.com/videos/taboo-1980-full-vintage-movie-14390985", + "review": 0, + "should_download": 0, + "title": "Tabu (1980 kompletter Retro-Film)", + "file_name": "Tabu (1980 kompletter Retro-Film) [14390985].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9d60df03-6dfa-4683-a624-10fe6162dcae.mp4" + }, + { + "id": "9dd578ba-a812-4b96-ada6-a43be8e681e9", + "created_date": "2024-07-25 07:29:45.229523", + "last_modified_date": "2024-07-25 07:29:45.229523", + "version": 0, + "url": "https://ge.xhamster.com/videos/little-darlings-retro-13590759", + "review": 0, + "should_download": 0, + "title": "Kleine Lieblinge Retro | xHamster", + "file_name": "Kleine Lieblinge Retro [13590759].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/9dd578ba-a812-4b96-ada6-a43be8e681e9.mp4" + }, + { + "id": "9df9a129-3eb0-4c6d-8d9a-5ece1bbb29af", + "created_date": "2024-07-25 07:29:47.457926", + "last_modified_date": "2024-07-25 07:29:47.457926", + "version": 0, + "url": "https://ge.xhamster.com/videos/roommate-didnt-know-i-was-back-home-xhr7DMI", + "review": 0, + "should_download": 0, + "title": "Mitbewohner wusste nicht, dass ich zu Hause war | xHamster", + "file_name": "Mitbewohner wusste nicht, dass ich zu Hause war [xhr7DMI].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9df9a129-3eb0-4c6d-8d9a-5ece1bbb29af.mp4" + }, + { + "id": "9e59cbb6-5b26-455d-aa58-6582561c77a6", + "created_date": "2024-08-08 00:54:26.820708", + "last_modified_date": "2024-08-16 11:10:41.816000", + "version": 1, + "url": "https://ge.xhamster.com/videos/fakeagentuk-huge-facial-for-hot-petite-librarian-at-casting-4781377", + "review": 0, + "should_download": 0, + "title": "Fakeagentuk Huge Facial for Hot Petite Librarian at Casting | xHamster", + "file_name": "FakeAgentUK Huge facial for hot petite librarian at casting-4781377.mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9e59cbb6-5b26-455d-aa58-6582561c77a6.mp4" + }, + { + "id": "9e822efc-11a7-416c-84c9-986ea236d0fe", + "created_date": "2024-08-28 23:21:54.374622", + "last_modified_date": "2024-08-28 23:21:54.374622", + "version": 0, + "url": "https://ge.xhamster.com/videos/joi-fr-my-roommate-catch-me-watching-porn-i-tell-him-to-jerk-off-in-front-of-me-xhfk1Zp", + "review": 0, + "should_download": 0, + "title": "Joi fr - mein mitbewohner erwischt mich beim porno, ich sage ihm, er soll vor mir wichsen | xHamster", + "file_name": "Joi fr - mein mitbewohner erwischt mich beim porno, ich sage ihm, er soll vor mir wichsen [xhfk1Zp].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/9e822efc-11a7-416c-84c9-986ea236d0fe.mp4" + }, + { + "id": "9e8884bc-decb-4422-85c8-86a397f29d0d", + "created_date": "2024-07-25 07:29:46.546529", + "last_modified_date": "2025-01-03 11:56:30.438000", + "version": 1, + "url": "https://ge.xhamster.com/videos/quiet-quiet-youll-wake-my-stepmom-hotel-guest-fucked-the-beautiful-hostess-and-her-cute-stepdaughter-xhWHS4j", + "review": 0, + "should_download": 0, + "title": "Ruhig, ruhig! Du wirst meine stiefmutter wecken! Hotelgast fickte die sch\u00f6ne gastgeberin und ihre s\u00fc\u00dfe stieftochter! | xHamster", + "file_name": "Ruhig, ruhig! Du wirst meine stiefmutter wecken! Hotelgast fickte die sch\u00f6ne gastgeberin und ihre s\u00fc\u00dfe stieftochter! [xhWHS4j].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9e8884bc-decb-4422-85c8-86a397f29d0d.mp4" + }, + { + "id": "9e8c56d1-15f8-4bd6-809e-38537e0d1c8e", + "created_date": "2024-07-25 07:29:46.493075", + "last_modified_date": "2024-07-25 07:29:46.493075", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-golden-age-of-danish-porno-1970-1974-9614343", + "review": 0, + "should_download": 0, + "title": "Das goldene Zeitalter des d\u00e4nischen Pornos 1970-1974 | xHamster", + "file_name": "Das goldene Zeitalter des d\u00e4nischen Pornos 1970-1974 [9614343].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9e8c56d1-15f8-4bd6-809e-38537e0d1c8e.mp4" + }, + { + "id": "9ea37128-7f93-4020-963b-193e3300ce6b", + "created_date": "2024-07-25 07:29:45.281636", + "last_modified_date": "2024-07-25 07:29:45.281636", + "version": 0, + "url": "https://ge.xhamster.com/videos/strange-family-1977-13694110", + "review": 0, + "should_download": 0, + "title": "Strange Family (1977) | xHamster", + "file_name": "Strange Family (1977) [13694110].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9ea37128-7f93-4020-963b-193e3300ce6b.mp4" + }, + { + "id": "9f030602-ef27-4b27-bf0b-223e80a5f8c9", + "created_date": "2024-12-29 23:53:27.956712", + "last_modified_date": "2024-12-29 23:53:27.956712", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-seduction-of-cindy-xhlnaIY", + "review": 0, + "should_download": 0, + "title": "Die Verf\u00fchrung von Cindy | xHamster", + "file_name": "Die Verf\u00fchrung von Cindy [xhlnaIY].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/9f030602-ef27-4b27-bf0b-223e80a5f8c9.mp4" + }, + { + "id": "9fc7db28-17bd-49f7-a896-c8b79251b45c", + "created_date": "2024-07-25 07:29:47.043835", + "last_modified_date": "2024-07-25 07:29:47.043835", + "version": 0, + "url": "https://ge.xhamster.com/videos/sexy-boat-full-movie-xhbet4F", + "review": 0, + "should_download": 0, + "title": "Sexy Boot (kompletter Film) | xHamster", + "file_name": "Sexy Boot (kompletter Film) [xhbet4F].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/9fc7db28-17bd-49f7-a896-c8b79251b45c.mp4" + }, + { + "id": "9fd234ee-3cad-4bdf-974d-527f49359930", + "created_date": "2024-07-25 07:29:45.552726", + "last_modified_date": "2024-07-25 07:29:45.552726", + "version": 0, + "url": "https://ge.xhamster.com/videos/mrs-sanders-wants-her-sexy-babysitter-to-fuck-her-husband-xhDMPwY", + "review": 0, + "should_download": 0, + "title": "Mrs Sanders will, dass ihr sexy Babysitter ihren Ehemann fickt | xHamster", + "file_name": "Mrs Sanders will, dass ihr sexy Babysitter ihren Ehemann fickt [xhDMPwY].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/9fd234ee-3cad-4bdf-974d-527f49359930.mp4" + }, + { + "id": "a0027f8d-c07c-460a-b6fd-4196641792fb", + "created_date": "2025-01-19 13:42:32.817625", + "last_modified_date": "2025-01-19 13:42:32.817632", + "version": 0, + "url": "https://ge.xhamster.com/videos/our-horny-step-brother-wouldnt-stop-bothering-us-so-we-let-him-fuck-us-xh6z62K", + "review": 0, + "should_download": 0, + "title": "Unser geiler stiefbruder w\u00fcrde nicht aufh\u00f6ren, uns zu bel\u00e4stigen, also haben wir ihn uns ficken lassen | xHamster", + "file_name": "Unser geiler stiefbruder w\u00fcrde nicht aufh\u00f6ren, uns zu bel\u00e4stigen, also haben wir ihn uns ficken lassen [xh6z62K].mp4", + "path": null, + "cloud_link": null + }, + { + "id": "a0294259-12bc-4610-9913-eb8e366e8e8b", + "created_date": "2024-07-25 07:29:46.519061", + "last_modified_date": "2024-07-25 07:29:46.519061", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsons-overactive-sex-drive-brianna-beach-xh2F1Bu", + "review": 0, + "should_download": 0, + "title": "Stiefsohns \u00fcberaktiver Sexualtrieb - Brianna Beach | xHamster", + "file_name": "Stiefsohns \u00fcberaktiver Sexualtrieb - Brianna Beach [xh2F1Bu].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a0294259-12bc-4610-9913-eb8e366e8e8b.mp4" + }, + { + "id": "a096b607-b352-44d0-afda-26ee0daaf42e", + "created_date": "2024-07-25 07:29:47.851038", + "last_modified_date": "2024-07-25 07:29:47.851038", + "version": 0, + "url": "https://ge.xhamster.com/videos/arschlocher-perversa-1990-xhtRIZQ", + "review": 0, + "should_download": 0, + "title": "Arschlocher - Perversa (1990) | xHamster", + "file_name": "Arschlocher - Perversa (1990) [xhtRIZQ].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a096b607-b352-44d0-afda-26ee0daaf42e.mp4" + }, + { + "id": "a0a6d6f6-c804-4958-b669-664b01cf0edf", + "created_date": "2024-07-25 07:29:44.355542", + "last_modified_date": "2024-07-25 07:29:44.355542", + "version": 0, + "url": "https://ge.xhamster.com/videos/fucking-amongst-friends-5554815", + "review": 0, + "should_download": 0, + "title": "Unter Freunden ficken | xHamster", + "file_name": "Unter Freunden ficken [5554815].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a0a6d6f6-c804-4958-b669-664b01cf0edf.mp4" + }, + { + "id": "a16fca93-afc3-4995-acd6-1332185b8b3f", + "created_date": "2024-07-25 07:29:44.632714", + "last_modified_date": "2024-07-25 07:29:44.632714", + "version": 0, + "url": "https://ge.xhamster.com/videos/bikini-girls-full-movie-xh4LKZb", + "review": 0, + "should_download": 0, + "title": "Bikini-M\u00e4dchen (kompletter Film) | xHamster", + "file_name": "Bikini-M\u00e4dchen (kompletter Film) [xh4LKZb].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a16fca93-afc3-4995-acd6-1332185b8b3f.mp4" + }, + { + "id": "a1758e04-d5b8-49c6-b2da-f4a6aa99c9dd", + "created_date": "2024-07-25 07:29:46.645132", + "last_modified_date": "2024-07-25 07:29:46.645132", + "version": 0, + "url": "https://ge.xhamster.com/videos/i-suck-my-lovers-dick-and-my-husband-licks-my-pussy-xhBXiRj", + "review": 0, + "should_download": 0, + "title": "Ich lutsche den Schwanz meines Liebhabers und mein Ehemann leckt meine Muschi. | xHamster", + "file_name": "Ich lutsche den Schwanz meines Liebhabers und mein Ehemann leckt meine Muschi. [xhBXiRj].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a1758e04-d5b8-49c6-b2da-f4a6aa99c9dd.mp4" + }, + { + "id": "a1d442bb-249d-4ec1-ac8a-10ffc34e1260", + "created_date": "2024-07-25 07:29:46.594634", + "last_modified_date": "2024-07-25 07:29:46.594634", + "version": 0, + "url": "https://ge.xhamster.com/videos/swimming-pool-sex-party-7-4842754", + "review": 0, + "should_download": 0, + "title": "Schwimmbad-Sex-Party 7! | xHamster", + "file_name": "Schwimmbad-Sex-Party 7! [4842754].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a1d442bb-249d-4ec1-ac8a-10ffc34e1260.mp4" + }, + { + "id": "a1f2bbc0-63ea-41bc-9c2f-bf411d0712b5", + "created_date": "2024-07-25 07:29:44.702879", + "last_modified_date": "2024-07-25 07:29:44.702879", + "version": 0, + "url": "https://ge.xhamster.com/videos/teen-orgy-1981-12884726", + "review": 0, + "should_download": 0, + "title": "Teenie-Orgie (1981) | xHamster", + "file_name": "Teenie-Orgie (1981) [12884726].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a1f2bbc0-63ea-41bc-9c2f-bf411d0712b5.mp4" + }, + { + "id": "a2503eff-7568-4e81-988a-fdfbbc5194c9", + "created_date": "2024-07-25 07:29:45.222202", + "last_modified_date": "2024-07-25 07:29:45.222202", + "version": 0, + "url": "https://ge.xhamster.com/videos/vacation-party-15005887", + "review": 0, + "should_download": 0, + "title": "Urlaubsparty. | xHamster", + "file_name": "Urlaubsparty. [15005887].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a2503eff-7568-4e81-988a-fdfbbc5194c9.mp4" + }, + { + "id": "a26c3219-735f-44fb-b8cb-cdeaa6057684", + "created_date": "2024-07-25 07:29:45.636506", + "last_modified_date": "2024-07-25 07:29:45.636506", + "version": 0, + "url": "https://ge.xhamster.com/videos/are-you-sperm-im-sorry-i-accidentally-xhEVovf", + "review": 0, + "should_download": 0, + "title": "Bist du sperma !? Es tut mir leid, dass ich versehentlich ... | xHamster", + "file_name": "Bist du sperma !\uff1f Es tut mir leid, dass ich versehentlich ... [xhEVovf].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a26c3219-735f-44fb-b8cb-cdeaa6057684.mp4" + }, + { + "id": "a271298c-015e-45a7-96b3-f42132cd3d6a", + "created_date": "2024-07-25 07:29:48.114805", + "last_modified_date": "2024-07-25 07:29:48.114805", + "version": 0, + "url": "https://ge.xhamster.com/videos/er-schrieb-suche-milf-urlaubsbegleitung-fuer-sex-jeden-tag-hat-er-mich-mehrmals-gebumst-xh1Lzmk", + "review": 0, + "should_download": 0, + "title": "Er Schrieb Suche MILF Urlaubsbegleitung Fuer Sex Jeden Tag Hat Er Mich Mehrmals Gebumst | xHamster", + "file_name": "Er schrieb Suche MILF Urlaubsbegleitung fuer Sex Jeden Tag hat er mich mehrmals gebumst [xh1Lzmk].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a271298c-015e-45a7-96b3-f42132cd3d6a.mp4" + }, + { + "id": "a2deae4a-ffe4-421a-adec-09b9b9a88c23", + "created_date": "2024-07-25 07:29:44.323597", + "last_modified_date": "2024-07-25 07:29:44.323597", + "version": 0, + "url": "https://ge.xhamster.com/videos/a-very-shy-milf-has-to-swallow-her-bosss-cum-to-get-hired-for-in-a-job-interview-xhcgbvO", + "review": 0, + "should_download": 0, + "title": "Eine sehr sch\u00fcchterne milf muss das sperma ihres chefs schlucken, um in einem vorstellungsgespr\u00e4ch eingestellt zu werden | xHamster", + "file_name": "Eine sehr sch\u00fcchterne milf muss das sperma ihres chefs schlucken, um in einem vorstellungsgespr\u00e4ch eingestellt zu werden [xhcgbvO].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/a2deae4a-ffe4-421a-adec-09b9b9a88c23.mp4" + }, + { + "id": "a3109427-1055-46b8-81d8-4b08cfb182b2", + "created_date": "2024-07-25 07:29:45.807677", + "last_modified_date": "2024-07-25 07:29:45.807677", + "version": 0, + "url": "https://ge.xhamster.com/videos/ali-rae-naked-pool-party-13682407", + "review": 0, + "should_download": 0, + "title": "Ali Rae nackte Pool-Party | xHamster", + "file_name": "Ali Rae nackte Pool-Party [13682407].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a3109427-1055-46b8-81d8-4b08cfb182b2.mp4" + }, + { + "id": "a3519c37-131b-44f7-8965-f87d37bf0ef4", + "created_date": "2024-07-25 07:29:47.498756", + "last_modified_date": "2024-07-25 07:29:47.498756", + "version": 0, + "url": "https://ge.xhamster.com/videos/all-in-xhE6huv", + "review": 0, + "should_download": 0, + "title": "Alles in !!! | xHamster", + "file_name": "Alles in !!! [xhE6huv].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a3519c37-131b-44f7-8965-f87d37bf0ef4.mp4" + }, + { + "id": "a37d6807-6ccc-436d-946f-4e42fe17a6be", + "created_date": "2024-11-10 16:53:33.495066", + "last_modified_date": "2024-11-10 16:53:33.495066", + "version": 0, + "url": "https://ge.xhamster.com/videos/flying-skirts-1986-france-us-dub-full-movie-dvd-rip-xhfKMkX", + "review": 0, + "should_download": 0, + "title": "Flying Rocks (1986, Frankreich, US-Synchron, kompletter Film, DVD-Rip) | xHamster", + "file_name": "Flying Rocks (1986, Frankreich, US-Synchron, kompletter Film, DVD-Rip) [xhfKMkX].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/a37d6807-6ccc-436d-946f-4e42fe17a6be.mp4" + }, + { + "id": "a3d7b30f-dea9-408f-aa23-f5fdcaf30b66", + "created_date": "2024-07-25 07:29:44.812168", + "last_modified_date": "2024-07-25 07:29:44.812168", + "version": 0, + "url": "https://ge.xhamster.com/videos/strip-fuck-it-with-malloy-chuck-candle-simon-and-addie-1828233", + "review": 0, + "should_download": 0, + "title": "Strip-Fick mit Malloy, Chuck, Candle, Simon und Addie ( | xHamster", + "file_name": "Strip-Fick mit Malloy, Chuck, Candle, Simon und Addie ( [1828233].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a3d7b30f-dea9-408f-aa23-f5fdcaf30b66.mp4" + }, + { + "id": "a3e24028-d4e7-4eda-8cbd-80443ec8eb63", + "created_date": "2025-01-16 19:59:44.564134", + "last_modified_date": "2025-01-16 19:59:44.564141", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-fun-11182011", + "review": 0, + "should_download": 0, + "title": "Familienspa\u00df | xHamster", + "file_name": "Familienspa\u00df [11182011].mp4", + "path": null, + "cloud_link": "/data/media/a3e24028-d4e7-4eda-8cbd-80443ec8eb63.mp4" + }, + { + "id": "a40aa8f5-c9c6-4565-9bc0-f1784305e806", + "created_date": "2024-07-25 07:29:44.850120", + "last_modified_date": "2024-07-25 07:29:44.850120", + "version": 0, + "url": "https://ge.xhamster.com/videos/madchen-von-nebenan-1-xh4FGUaG", + "review": 0, + "should_download": 0, + "title": "Madchen Von Nebenan 1, Free In German Porn e0 | xHamster", + "file_name": "Madchen von nebenan 1 [xh4FGUaG].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/a40aa8f5-c9c6-4565-9bc0-f1784305e806.mp4" + }, + { + "id": "a434d04d-c03c-43dc-8666-c1561f212c41", + "created_date": "2024-07-25 07:29:47.521419", + "last_modified_date": "2024-07-25 07:29:47.521419", + "version": 0, + "url": "https://ge.xhamster.com/videos/sisswap-naughty-best-friends-both-had-lusty-feeling-for-each-other-s-stepbros-swapped-to-fuck-them-xhFBLWG", + "review": 0, + "should_download": 0, + "title": "Sisswap ist eine freche beste freunde, beide hatten lustvolles gef\u00fchl f\u00fcr die stiefbruer des anderen, getauscht, um sie zu ficken | xHamster", + "file_name": "Sisswap ist eine freche beste freunde, beide hatten lustvolles gef\u00fchl f\u00fcr die stiefbruer des anderen, getauscht, um sie zu ficken [xhFBLWG].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a434d04d-c03c-43dc-8666-c1561f212c41.mp4" + }, + { + "id": "a46edafe-c36c-4f9f-ad03-ef907867ed08", + "created_date": "2024-07-25 07:29:45.333273", + "last_modified_date": "2024-07-25 07:29:45.333273", + "version": 0, + "url": "https://ge.xhamster.com/videos/nasse-junge-schnecken-1992-full-movie-xhOtQro", + "review": 0, + "should_download": 0, + "title": "Nasse Junge Schnecken 1992 Full Movie, Porn eb | xHamster", + "file_name": "Nasse Junge Schnecken 1992 Full Movie [xhOtQro].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/a46edafe-c36c-4f9f-ad03-ef907867ed08.mp4" + }, + { + "id": "a499bc36-019f-4f40-969e-975629ab678b", + "created_date": "2024-07-25 07:29:47.937782", + "last_modified_date": "2024-07-25 07:29:47.937782", + "version": 0, + "url": "https://ge.xhamster.com/videos/big-boobed-stepsister-sneaks-in-your-room-xhFgp7k", + "review": 0, + "should_download": 0, + "title": "Stiefschwester mit gro\u00dfen M\u00f6psen schleicht sich in dein Zimmer | xHamster", + "file_name": "Stiefschwester mit gro\u00dfen M\u00f6psen schleicht sich in dein Zimmer [xhFgp7k].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a499bc36-019f-4f40-969e-975629ab678b.mp4" + }, + { + "id": "a4a3af84-7661-4bb4-b790-bd1759ad9749", + "created_date": "2025-01-16 19:59:49.625422", + "last_modified_date": "2025-01-16 19:59:49.625428", + "version": 0, + "url": "https://ge.xhamster.com/videos/forbidden-desires-xhcmdLt", + "review": 0, + "should_download": 0, + "title": "Verbotene W\u00fcnsche | xHamster", + "file_name": "Verbotene W\u00fcnsche [xhcmdLt].mp4", + "path": null, + "cloud_link": "/data/media/a4a3af84-7661-4bb4-b790-bd1759ad9749.mp4" + }, + { + "id": "a4ac8fdb-9757-4e37-af58-8b2a2175f32c", + "created_date": "2024-07-25 07:29:46.729526", + "last_modified_date": "2024-07-25 07:29:46.729526", + "version": 0, + "url": "https://ge.xhamster.com/videos/watching-porn-with-my-hot-stepmom-ended-up-fucking-her-xhqxPt7", + "review": 0, + "should_download": 0, + "title": "Porno gucken mit meiner hei\u00dfen stiefmutter - ich habe sie am ende gefickt | xHamster", + "file_name": "Porno gucken mit meiner hei\u00dfen stiefmutter - ich habe sie am ende gefickt [xhqxPt7].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a4ac8fdb-9757-4e37-af58-8b2a2175f32c.mp4" + }, + { + "id": "a5ac88d8-b211-49cf-a056-24aa49a07aae", + "created_date": "2024-07-25 07:29:46.328206", + "last_modified_date": "2024-07-25 07:29:46.328206", + "version": 0, + "url": "https://ge.xhamster.com/videos/strict-stepdad-destroyed-his-stepdaughters-madi-collins-butt-and-pussy-in-their-rough-threesome-with-maid-crystal-rush-xhhTOEb", + "review": 0, + "should_download": 0, + "title": "Strenge stiefvater zerst\u00f6rte madi Collins hintern und muschi seiner stieftochter in ihrem groben dreier mit zimmerm\u00e4dchen Crystal Rush. | xHamster", + "file_name": "Strenge stiefvater zerst\u00f6rte madi Collins hintern und muschi seiner stieftochter in ihrem groben dreier mit zimmerm\u00e4dchen Crystal Rush. [xhhTOEb].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a5ac88d8-b211-49cf-a056-24aa49a07aae.mp4" + }, + { + "id": "a5b1907a-3bda-47f1-b096-bb3f81b1b8ef", + "created_date": "2024-07-25 07:29:44.768803", + "last_modified_date": "2024-07-25 07:29:44.768803", + "version": 0, + "url": "https://ge.xhamster.com/videos/sex-addicted-secretary-masturbates-a-lot-while-at-work-xh9IYht", + "review": 0, + "should_download": 0, + "title": "Sexs\u00fcchtige Sekret\u00e4rin masturbiert viel w\u00e4hrend der Arbeit | xHamster", + "file_name": "Sexs\u00fcchtige Sekret\u00e4rin masturbiert viel w\u00e4hrend der Arbeit [xh9IYht].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a5b1907a-3bda-47f1-b096-bb3f81b1b8ef.mp4" + }, + { + "id": "a5d9e6bc-0f0e-421e-aceb-f231ae0ff123", + "created_date": "2024-07-25 07:29:44.570789", + "last_modified_date": "2024-07-25 07:29:44.570789", + "version": 0, + "url": "https://ge.xhamster.com/videos/hot-student-gets-fucked-by-busty-hentai-teacher-xhO6Zdc", + "review": 0, + "should_download": 0, + "title": "Hei\u00dfe Studentin wird von vollbusiger Hentai-Lehrerin gefickt | xHamster", + "file_name": "Hei\u00dfe Studentin wird von vollbusiger Hentai-Lehrerin gefickt [xhO6Zdc].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a5d9e6bc-0f0e-421e-aceb-f231ae0ff123.mp4" + }, + { + "id": "a6064845-5409-4865-9bbe-21e65c662e9c", + "created_date": "2024-11-10 16:53:33.489417", + "last_modified_date": "2024-11-10 16:53:33.489417", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-diary-1999-xhtTgTN", + "review": 0, + "should_download": 0, + "title": "Das Tagebuch (1999) | xHamster", + "file_name": "Das Tagebuch (1999) [xhtTgTN].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/a6064845-5409-4865-9bbe-21e65c662e9c.mp4" + }, + { + "id": "a6290082-53b4-4489-9d23-8a3704f5ac82", + "created_date": "2024-07-25 07:29:46.922354", + "last_modified_date": "2024-07-25 07:29:46.922354", + "version": 0, + "url": "https://ge.xhamster.com/videos/i-confuse-my-horny-stepsister-with-my-girlfriend-and-i-end-up-fucking-her-hard-until-i-cum-in-her-xhkhlpc", + "review": 0, + "should_download": 0, + "title": "Ich verwechsle meine geile Stiefschwester mit meiner Freundin und ich ficke sie am Ende hart, bis ich in sie komme | xHamster", + "file_name": "Ich verwechsle meine geile Stiefschwester mit meiner Freundin und ich ficke sie am Ende hart, bis ich in sie komme [xhkhlpc].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a6290082-53b4-4489-9d23-8a3704f5ac82.mp4" + }, + { + "id": "a66f1640-fc35-4163-a2b7-aa9004d0bdd5", + "created_date": "2024-07-25 07:29:45.982409", + "last_modified_date": "2024-07-25 07:29:45.982409", + "version": 0, + "url": "https://ge.xhamster.com/videos/mom-takes-charge-s8-e8-xhIwZux", + "review": 0, + "should_download": 0, + "title": "Mutter \u00fcbernimmt die f\u00fchrung - s8: e8 | xHamster", + "file_name": "Mutter \u00fcbernimmt die f\u00fchrung - s8\uff1a e8 [xhIwZux].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a66f1640-fc35-4163-a2b7-aa9004d0bdd5.mp4" + }, + { + "id": "a67e2cad-d534-400f-848c-0da300f973b5", + "created_date": "2024-07-25 07:29:46.188633", + "last_modified_date": "2024-07-25 07:29:46.188633", + "version": 0, + "url": "https://ge.xhamster.com/videos/lily-larimar-tells-stepbro-your-going-to-have-to-take-all-of-your-clothes-off-s14-e1-xhtoeBA", + "review": 0, + "should_download": 0, + "title": "Lily Larimar sagt zu Stiefbruder: \"Du musst alle deine Kleider ausziehen m\u00fcssen\" - s14: e1 | xHamster", + "file_name": "Lily Larimar sagt zu Stiefbruder\uff1a \uff02Du musst alle deine Kleider ausziehen m\u00fcssen\uff02 - s14\uff1a e1 [xhtoeBA].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a67e2cad-d534-400f-848c-0da300f973b5.mp4" + }, + { + "id": "a6982216-26b0-4d5e-9ce4-0a073fd3c776", + "created_date": "2024-07-25 07:29:44.555554", + "last_modified_date": "2024-07-25 07:29:44.555554", + "version": 0, + "url": "https://ge.xhamster.com/videos/summertime-blossom-part-1-a-new-blake-aften-opal-hazel-moore-blake-blossom-ginger-grey-bffs-xhqFPrM", + "review": 0, + "should_download": 0, + "title": "Summertime blossom teil 1: a new blake - oft opal, hazel moore, blake blossom, ginger grey - BFFS | xHamster", + "file_name": "Summertime blossom teil 1\uff1a a new blake - oft opal, hazel moore, blake blossom, ginger grey - BFFS [xhqFPrM].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a6982216-26b0-4d5e-9ce4-0a073fd3c776.mp4" + }, + { + "id": "a6e22a1b-5b5e-42ef-b495-e599654cdaf6", + "created_date": "2024-07-25 07:29:47.008739", + "last_modified_date": "2025-01-03 11:56:34.026000", + "version": 1, + "url": "https://ge.xhamster.com/videos/summer-of-72-higher-quality-14494064", + "review": 0, + "should_download": 0, + "title": "Sommer 1972 (h\u00f6here Qualit\u00e4t) | xHamster", + "file_name": "Sommer 1972 (h\u00f6here Qualit\u00e4t) [14494064].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a6e22a1b-5b5e-42ef-b495-e599654cdaf6.mp4" + }, + { + "id": "a7130fe6-2a75-4847-9195-bd49c55c6fdb", + "created_date": "2024-07-25 07:29:46.821279", + "last_modified_date": "2024-07-25 07:29:46.821279", + "version": 0, + "url": "https://ge.xhamster.com/videos/fuck-my-wife-with-me-2-5527462", + "review": 0, + "should_download": 0, + "title": "Fick meine Frau mit mir # 2 | xHamster", + "file_name": "Fick meine Frau mit mir # 2 [5527462].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a7130fe6-2a75-4847-9195-bd49c55c6fdb.mp4" + }, + { + "id": "a78ec6c1-99c3-43dd-a2f5-92b60a7a26d7", + "created_date": "2024-07-25 07:29:45.393655", + "last_modified_date": "2024-07-25 07:29:45.393655", + "version": 0, + "url": "https://ge.xhamster.com/videos/college-teens-fuck-in-dorm-group-with-coeds-7373208", + "review": 0, + "should_download": 0, + "title": "College-Teenager ficken in Wohnheim-Gruppe mit Studentinnen | xHamster", + "file_name": "College-Teenager ficken in Wohnheim-Gruppe mit Studentinnen [7373208].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a78ec6c1-99c3-43dd-a2f5-92b60a7a26d7.mp4" + }, + { + "id": "a7eefa49-4e9b-47d7-a696-46cf80e2df25", + "created_date": "2024-07-25 07:29:44.420704", + "last_modified_date": "2024-07-25 07:29:44.420704", + "version": 0, + "url": "https://ge.xhamster.com/videos/sexboat-1980-xhTuL9v", + "review": 0, + "should_download": 0, + "title": "Sexboat (1980) | xHamster", + "file_name": "Sexboat (1980) [xhTuL9v].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a7eefa49-4e9b-47d7-a696-46cf80e2df25.mp4" + }, + { + "id": "a80f1c63-ff2c-482b-876f-df4f2fbfd9d5", + "created_date": "2024-07-25 07:29:47.243258", + "last_modified_date": "2024-07-25 07:29:47.243258", + "version": 0, + "url": "https://ge.xhamster.com/videos/devilsfilm-rich-neighbors-swap-wives-during-football-game-xhFqS2u", + "review": 0, + "should_download": 0, + "title": "In Devilsfilm tauschen reiche Nachbarn beim Fu\u00dfballspiel Ehefrauen aus | xHamster", + "file_name": "In Devilsfilm tauschen reiche Nachbarn beim Fu\u00dfballspiel Ehefrauen aus [xhFqS2u].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a80f1c63-ff2c-482b-876f-df4f2fbfd9d5.mp4" + }, + { + "id": "a86c6c7b-5633-49cf-a68e-867b268f5dce", + "created_date": "2024-07-25 07:29:46.614645", + "last_modified_date": "2024-07-25 07:29:46.614645", + "version": 0, + "url": "https://ge.xhamster.com/videos/you-can-join-me-if-you-want-to-kasey-miller-12975348", + "review": 0, + "should_download": 0, + "title": "Sie k\u00f6nnen sich mir anschlie\u00dfen, wenn Sie wollen - Kasey Miller | xHamster", + "file_name": "Sie k\u00f6nnen sich mir anschlie\u00dfen, wenn Sie wollen - Kasey Miller [12975348].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a86c6c7b-5633-49cf-a68e-867b268f5dce.mp4" + }, + { + "id": "a879ec91-4545-4135-90d4-5f47f2f812e6", + "created_date": "2024-11-10 16:53:33.495875", + "last_modified_date": "2024-11-10 16:53:33.495875", + "version": 0, + "url": "https://ge.xhamster.com/videos/hallo-taxi-full-movie-xhJL30l", + "review": 0, + "should_download": 0, + "title": "Hallo Taxi! (kompletter Film) | xHamster", + "file_name": "Hallo Taxi! (kompletter Film) [xhJL30l].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/a879ec91-4545-4135-90d4-5f47f2f812e6.mp4" + }, + { + "id": "a8df179e-fa46-493c-a6f8-1fba6b3a901f", + "created_date": "2024-09-24 08:11:39.008727", + "last_modified_date": "2024-10-21 16:31:13.307000", + "version": 1, + "url": "https://ge.xhamster.com/videos/die-pension-mit-der-geilen-milf-5718112", + "review": 0, + "should_download": 0, + "title": "Die Pension Mit Der Geilen MILF, Free HD Porn 62 | xHamster", + "file_name": "Die Pension mit der geilen Milf [5718112].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/a8df179e-fa46-493c-a6f8-1fba6b3a901f.mp4" + }, + { + "id": "a8e87e11-e015-404e-a8e6-397645ada52c", + "created_date": "2024-07-25 07:29:46.535764", + "last_modified_date": "2024-07-25 07:29:46.535764", + "version": 0, + "url": "https://ge.xhamster.com/videos/mom-says-april-fools-your-dick-was-inside-me-xh1HNRC", + "review": 0, + "should_download": 0, + "title": "Mutter sagt, \"Aprilscherz? Dein Schwanz war in mir!\" | xHamster", + "file_name": "Mutter sagt, "Aprilscherz\uff1f Dein Schwanz war in mir!" [xh1HNRC].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a8e87e11-e015-404e-a8e6-397645ada52c.mp4" + }, + { + "id": "a8ee4910-d674-4147-a63d-4f82db7e8994", + "created_date": "2024-07-25 07:29:44.255302", + "last_modified_date": "2024-07-25 07:29:44.255302", + "version": 0, + "url": "https://ge.xhamster.com/videos/dirty-teens-start-the-summer-right-with-some-sex-party-6424570", + "review": 0, + "should_download": 0, + "title": "Schmutzige Teenager beginnen den Sommer direkt mit einer Sexparty | xHamster", + "file_name": "Schmutzige Teenager beginnen den Sommer direkt mit einer Sexparty [6424570].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a8ee4910-d674-4147-a63d-4f82db7e8994.mp4" + }, + { + "id": "a8f0a31e-a958-4d6b-9496-fdc6cf919a86", + "created_date": "2024-07-25 07:29:44.949902", + "last_modified_date": "2024-07-25 07:29:44.949902", + "version": 0, + "url": "https://ge.xhamster.com/videos/horny-slut-seduces-her-roommate-and-makes-him-cum-xhByTjb", + "review": 0, + "should_download": 0, + "title": "Notgeile Schlampe verf\u00fchrt ihren Mitbewohner und bring ihn zum Abspritzen | xHamster", + "file_name": "Notgeile Schlampe verf\u00fchrt ihren Mitbewohner und bring ihn zum Abspritzen [xhByTjb].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/a8f0a31e-a958-4d6b-9496-fdc6cf919a86.mp4" + }, + { + "id": "a8f5e91f-0266-46eb-a627-4383fdb96b28", + "created_date": "2024-07-25 07:29:45.215101", + "last_modified_date": "2024-07-25 07:29:45.215101", + "version": 0, + "url": "https://ge.xhamster.com/videos/a-work-accident-become-a-scene-sex-at-the-office-quick-fuck-boss-and-secretary-he-cum-twice-in-her-pussy-xhUnLbU", + "review": 0, + "should_download": 0, + "title": "Ein Arbeitsunfall wird zur Sexszene im B\u00fcro. Schneller Fick zwischen Chef und Sekret\u00e4rin, er spritzt ihr zweimal in die Muschi | xHamster", + "file_name": "Ein Arbeitsunfall wird zur Sexszene im B\u00fcro. Schneller Fick zwischen Chef und Sekret\u00e4rin, er spritzt ihr zweimal in die Muschi [xhUnLbU].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a8f5e91f-0266-46eb-a627-4383fdb96b28.mp4" + }, + { + "id": "a98f6751-63b4-4b21-855a-01d798b1d905", + "created_date": "2024-07-25 07:29:48.033800", + "last_modified_date": "2024-07-25 07:29:48.033800", + "version": 0, + "url": "https://ge.xhamster.com/videos/meine-suendhafte-tante-xh7YP9d", + "review": 0, + "should_download": 0, + "title": "Meine Suendhafte Tante, Free Porn Video 0b | xHamster", + "file_name": "Meine Suendhafte Tante [xh7YP9d].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a98f6751-63b4-4b21-855a-01d798b1d905.mp4" + }, + { + "id": "a9b70978-8ea1-41a0-8c97-5c203df401a8", + "created_date": "2024-07-25 07:29:45.629393", + "last_modified_date": "2024-07-25 07:29:45.629393", + "version": 0, + "url": "https://ge.xhamster.com/videos/geiler-vierer-im-zug-1377950", + "review": 0, + "should_download": 0, + "title": "Geiler Vierer Im Zug: Free Amateur Porn Video cb | xHamster", + "file_name": "Geiler Vierer im Zug [1377950].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a9b70978-8ea1-41a0-8c97-5c203df401a8.mp4" + }, + { + "id": "a9d69e97-8462-46c5-a031-1ff1f5fb9bbc", + "created_date": "2024-07-25 07:29:44.303800", + "last_modified_date": "2024-07-25 07:29:44.303800", + "version": 0, + "url": "https://ge.xhamster.com/videos/dancing-in-the-rain-4963585", + "review": 0, + "should_download": 0, + "title": "Tanzen im Regen | xHamster", + "file_name": "Tanzen im Regen [4963585].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a9d69e97-8462-46c5-a031-1ff1f5fb9bbc.mp4" + }, + { + "id": "a9fc39b4-682f-44a4-b80e-f91732556531", + "created_date": "2024-07-25 07:29:46.937813", + "last_modified_date": "2024-07-25 07:29:46.937813", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsis-misses-the-cock-xhELzCj", + "review": 0, + "should_download": 0, + "title": "Stiefschwester vermisst den Schwanz | xHamster", + "file_name": "Stiefschwester vermisst den Schwanz [xhELzCj].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/a9fc39b4-682f-44a4-b80e-f91732556531.mp4" + }, + { + "id": "aa3c38d6-110f-40bd-b3e1-b13face62221", + "created_date": "2024-07-25 07:29:44.670678", + "last_modified_date": "2024-07-25 07:29:44.670678", + "version": 0, + "url": "https://ge.xhamster.com/videos/german-teen-picked-up-at-beach-for-threesome-ffm-xhloGpt", + "review": 0, + "should_download": 0, + "title": "Deutsche teen anal abgeschleppt am strand zum Dreier ffm | xHamster", + "file_name": "Deutsche teen anal abgeschleppt am strand zum Dreier ffm [xhloGpt].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/aa3c38d6-110f-40bd-b3e1-b13face62221.mp4" + }, + { + "id": "aa8e2700-521f-4b6e-9beb-c5619c7bdacd", + "created_date": "2024-07-25 07:29:46.336250", + "last_modified_date": "2024-07-25 07:29:46.336250", + "version": 0, + "url": "https://ge.xhamster.com/videos/bride4k-wrong-but-kinda-right-xh3FIwK", + "review": 0, + "should_download": 0, + "title": "Bride4k. Falsch, aber irgendwie richtig | xHamster", + "file_name": "Bride4k. Falsch, aber irgendwie richtig [xh3FIwK].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/aa8e2700-521f-4b6e-9beb-c5619c7bdacd.mp4" + }, + { + "id": "aaa02634-512d-4e27-9309-d182ab3045a7", + "created_date": "2024-07-25 07:29:45.934040", + "last_modified_date": "2024-07-25 07:29:45.934040", + "version": 0, + "url": "https://ge.xhamster.com/videos/hot-foursome-with-double-penetration-12476216", + "review": 0, + "should_download": 0, + "title": "Hei\u00dfer Vierer mit Doppelpenetration | xHamster", + "file_name": "Hei\u00dfer Vierer mit Doppelpenetration [12476216].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/aaa02634-512d-4e27-9309-d182ab3045a7.mp4" + }, + { + "id": "aaf0d8de-72f6-4e1f-8c06-404140f8630b", + "created_date": "2024-07-25 07:29:46.042118", + "last_modified_date": "2024-07-25 07:29:46.042118", + "version": 0, + "url": "https://ge.xhamster.com/videos/erotic-dinner-party-turns-into-orgy-upscaled-to-4k-xhVAZGT", + "review": 0, + "should_download": 0, + "title": "Erotische Dinnerpartys werden zu Orgien, auf 4k hochskaliert | xHamster", + "file_name": "Erotische Dinnerpartys werden zu Orgien, auf 4k hochskaliert [xhVAZGT].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/aaf0d8de-72f6-4e1f-8c06-404140f8630b.mp4" + }, + { + "id": "ab1c8d88-1724-4444-b4a0-19c4ca0f3ed3", + "created_date": "2024-08-28 23:21:54.354190", + "last_modified_date": "2024-08-28 23:21:54.354190", + "version": 0, + "url": "https://ge.xhamster.com/videos/sex-party-after-dinner-2559646", + "review": 0, + "should_download": 0, + "title": "Sexparty nach dem Abendessen | xHamster", + "file_name": "Sexparty nach dem Abendessen [2559646].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/ab1c8d88-1724-4444-b4a0-19c4ca0f3ed3.mp4" + }, + { + "id": "abab052a-8b10-4cfc-a922-a8333e0bb364", + "created_date": "2024-12-29 23:53:27.929108", + "last_modified_date": "2024-12-29 23:53:27.929108", + "version": 0, + "url": "https://ge.xhamster.com/videos/daddy-xh3DupT", + "review": 0, + "should_download": 0, + "title": "Papi | xHamster", + "file_name": "Papi [xh3DupT].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/abab052a-8b10-4cfc-a922-a8333e0bb364.mp4" + }, + { + "id": "abd02714-d938-43bb-8039-dace47e6249e", + "created_date": "2024-07-25 07:29:47.461434", + "last_modified_date": "2024-07-25 07:29:47.461434", + "version": 0, + "url": "https://ge.xhamster.desi/videos/religious-stepmom-kit-mercer-invites-stepdaughter-allie-nicole-for-a-midnight-obedience-lesson-xhDS8Iy", + "review": 0, + "should_download": 0, + "title": "Religi\u00f6se Stiefmutter Mercer l\u00e4dt Stieftochter Allie Nicole zu einer Mitternachtsgehorsam-Lektion ein | xHamster", + "file_name": "Religi\u00f6se Stiefmutter Mercer l\u00e4dt Stieftochter Allie Nicole zu einer Mitternachtsgehorsam-Lektion ein [xhDS8Iy].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/abd02714-d938-43bb-8039-dace47e6249e.mp4" + }, + { + "id": "ac203249-b982-4070-955a-39dd33cd01a9", + "created_date": "2024-12-29 23:53:27.927365", + "last_modified_date": "2024-12-29 23:53:27.927365", + "version": 0, + "url": "https://ge.xhamster.com/videos/botr-tonights-the-night-for-a-family-fuck-11677103", + "review": 0, + "should_download": 0, + "title": "Botr, heute Abend ist die Nacht f\u00fcr einen Familienfick! | xHamster", + "file_name": "Botr, heute Abend ist die Nacht f\u00fcr einen Familienfick! [11677103].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/ac203249-b982-4070-955a-39dd33cd01a9.mp4" + }, + { + "id": "ac98985a-5054-4a98-8969-a7f64a1f487b", + "created_date": "2024-10-21 15:08:43.547204", + "last_modified_date": "2024-10-21 16:31:20.049000", + "version": 1, + "url": "https://ge.xhamster.com/videos/two-amazing-looking-chicks-from-germany-pleasing-some-hard-cocks-at-the-bar-xh5cnma", + "review": 0, + "should_download": 0, + "title": "Zwei erstaunlich aussehende K\u00fcken aus Deutschland erfreuen einige harte Schw\u00e4nze an der Bar | xHamster", + "file_name": "Zwei erstaunlich aussehende K\u00fcken aus Deutschland erfreuen einige harte Schw\u00e4nze an der Bar [xh5cnma].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/ac98985a-5054-4a98-8969-a7f64a1f487b.mp4" + }, + { + "id": "acbf1549-0cfd-4fb5-acb1-032c15b55f12", + "created_date": "2024-07-25 07:29:46.514503", + "last_modified_date": "2024-07-25 07:29:46.514503", + "version": 0, + "url": "https://ge.xhamster.com/videos/das-sex-abitur-versaute-schulmadchen-traume-2-1978-5850792", + "review": 0, + "should_download": 0, + "title": "Das Sex Abitur - Versaute Schulmadchen Traume 2 1978 | xHamster", + "file_name": "Das Sex Abitur - Versaute Schulmadchen Traume 2 (1978) [5850792].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/acbf1549-0cfd-4fb5-acb1-032c15b55f12.mp4" + }, + { + "id": "acf0ecf9-699c-4f97-9d14-c8f66ee1e5ea", + "created_date": "2024-10-21 15:08:43.552109", + "last_modified_date": "2024-10-21 16:31:25.892000", + "version": 1, + "url": "https://ge.xhamster.com/videos/horny-housewife-rammed-by-2-guys-xhiRKFa", + "review": 0, + "should_download": 0, + "title": "Geile Hausfrau von 2 Typen gerammt | xHamster", + "file_name": "Geile Hausfrau von 2 Typen gerammt [xhiRKFa].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/acf0ecf9-699c-4f97-9d14-c8f66ee1e5ea.mp4" + }, + { + "id": "ad061d6e-e11a-47a8-801a-8be95886c030", + "created_date": "2024-07-25 07:29:45.922920", + "last_modified_date": "2024-07-25 07:29:45.922920", + "version": 0, + "url": "https://ge.xhamster.com/videos/horny-stepdaughter-daisy-stone-and-milf-athena-anderson-try-anal-foursome-with-stepbrother-and-stepdad-on-wedding-anniversary-xh4cwD4p", + "review": 0, + "should_download": 0, + "title": "Geile stieftochter Daisy Stone und MILF Athena Anderson versuchen anal-vierer mit stiefbruder und stiefvater zum hochzeitstag | xHamster", + "file_name": "Geile stieftochter Daisy Stone und MILF Athena Anderson versuchen anal-vierer mit stiefbruder und stiefvater zum hochzeitstag [xh4cwD4p].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ad061d6e-e11a-47a8-801a-8be95886c030.mp4" + }, + { + "id": "ad06ee1b-046a-4ad9-9b22-694dfe18ff43", + "created_date": "2024-07-25 07:29:47.707918", + "last_modified_date": "2024-07-25 07:29:47.707918", + "version": 0, + "url": "https://ge.xhamster.com/videos/auf-der-heidi-gibts-koa-sund-teil-1-8550043", + "review": 0, + "should_download": 0, + "title": "Auf Der Heidi Gibts Koa Sund Teil 1, Free Porn 8b | xHamster", + "file_name": "Auf Der Heidi gibts koa Sund teil 1 [8550043].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ad06ee1b-046a-4ad9-9b22-694dfe18ff43.mp4" + }, + { + "id": "ad0e2485-ea00-4586-9228-069a4c0dae6d", + "created_date": "2024-07-25 07:29:47.303442", + "last_modified_date": "2024-07-25 07:29:47.303442", + "version": 0, + "url": "https://ge.xhamster.com/videos/small-village-horny-schools-11170342", + "review": 0, + "should_download": 0, + "title": "Kleines Dorf, geile Schulen | xHamster", + "file_name": "Kleines Dorf, geile Schulen [11170342].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ad0e2485-ea00-4586-9228-069a4c0dae6d.mp4" + }, + { + "id": "ad319532-4da9-4593-88b3-bd381f513474", + "created_date": "2024-08-16 12:24:28.483000", + "last_modified_date": "2024-08-16 12:24:28.483000", + "version": 0, + "url": "https://ge.xhamster.com/videos/i-had-to-have-sex-with-my-husbands-friend-because-of-a-bet-but-i-got-horny-and-ended-up-doing-dp-with-both-of-them-xhneeoz", + "review": 0, + "should_download": 0, + "title": "Ich musste wegen einer wette sex mit dem freund meines mannes haben, aber ich wurde geil und habe am ende mit beiden doppelpenetration gemacht | xHamster", + "file_name": "Ich musste wegen einer wette sex mit dem freund meines mannes haben, aber ich wurde geil und habe am ende mit beiden doppelpenetration gemacht [xhneeoz].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/ad319532-4da9-4593-88b3-bd381f513474.mp4" + }, + { + "id": "ad4f4ec5-7ef9-4894-86c7-dccb682e467d", + "created_date": "2024-07-25 07:29:47.612962", + "last_modified_date": "2024-07-25 07:29:47.612962", + "version": 0, + "url": "https://ge.xhamster.com/videos/you-cant-fuck-the-guests-but-you-can-fuck-me-madi-collins-tempts-stepbro-s9-e1-xh2jqKB", + "review": 0, + "should_download": 0, + "title": "\"Du kannst die G\u00e4ste nicht ficken, aber du KANNST mich ficken\" Madi Collins verf\u00fchrt Stiefbruder - S9:E1 | xHamster", + "file_name": "\uff02Du kannst die G\u00e4ste nicht ficken, aber du KANNST mich ficken\uff02 Madi Collins verf\u00fchrt Stiefbruder - S9\uff1aE1 [xh2jqKB].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ad4f4ec5-7ef9-4894-86c7-dccb682e467d.mp4" + }, + { + "id": "ada32ec7-eba4-4f32-b7d2-5d1e949f0cb7", + "created_date": "2024-07-25 07:29:45.131833", + "last_modified_date": "2024-07-25 07:29:45.131833", + "version": 0, + "url": "https://ge.xhamster.com/videos/hot-retro-outdoor-group-sex-xhiOaBJ", + "review": 0, + "should_download": 0, + "title": "Hei\u00dfer Retro-Gruppensex im Freien | xHamster", + "file_name": "Hei\u00dfer Retro-Gruppensex im Freien [xhiOaBJ].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ada32ec7-eba4-4f32-b7d2-5d1e949f0cb7.mp4" + }, + { + "id": "adb80061-b9f1-4524-afe6-c2ec4472288d", + "created_date": "2024-07-25 07:29:46.889086", + "last_modified_date": "2024-07-25 07:29:46.889086", + "version": 0, + "url": "https://ge.xhamster.com/videos/bruederchen-komm-fick-mit-mir-3501015", + "review": 0, + "should_download": 0, + "title": "Bruederchen Komm Fick Mit Mir, Free Anal Porn 6d | xHamster", + "file_name": "Bruederchen Komm Fick Mit Mir [3501015].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/adb80061-b9f1-4524-afe6-c2ec4472288d.mp4" + }, + { + "id": "addaabe2-fb53-4d34-a2dc-84f07133a40e", + "created_date": "2024-07-25 07:29:45.646929", + "last_modified_date": "2024-07-25 07:29:45.646929", + "version": 0, + "url": "https://ge.xhamster.com/videos/college-teens-pussyfucked-before-cum-in-mouth-5705076", + "review": 0, + "should_download": 0, + "title": "College-Teenager vor dem Abspritzen in den Mund Muschi gefickt | xHamster", + "file_name": "College-Teenager vor dem Abspritzen in den Mund Muschi gefickt [5705076].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/addaabe2-fb53-4d34-a2dc-84f07133a40e.mp4" + }, + { + "id": "adf672b8-d475-4bbb-9808-3766859b35bf", + "created_date": "2024-10-07 20:47:56.420508", + "last_modified_date": "2024-10-21 16:31:30.267000", + "version": 1, + "url": "https://ge.xhamster.com/videos/anal-sex-danish-schoolgirls-4-9723188", + "review": 0, + "should_download": 0, + "title": "Analsex (d\u00e4nische Schulm\u00e4dchen 4) | xHamster", + "file_name": "Analsex (d\u00e4nische Schulm\u00e4dchen 4) [9723188].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/adf672b8-d475-4bbb-9808-3766859b35bf.mp4" + }, + { + "id": "ae05e7ea-ba22-4936-8317-606570b16ccb", + "created_date": "2024-07-25 07:29:44.532887", + "last_modified_date": "2024-07-25 07:29:44.532887", + "version": 0, + "url": "https://ge.xhamster.com/videos/teen-threesome-with-pepper-and-lexi-10085050", + "review": 0, + "should_download": 0, + "title": "Teenie-Dreier mit Pfeffer und Lexi | xHamster", + "file_name": "Teenie-Dreier mit Pfeffer und Lexi [10085050].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ae05e7ea-ba22-4936-8317-606570b16ccb.mp4" + }, + { + "id": "ae2e76ac-39ee-43b0-8321-badb9e4b0afa", + "created_date": "2024-07-25 07:29:47.040228", + "last_modified_date": "2024-07-25 07:29:47.040228", + "version": 0, + "url": "https://ge.xhamster.com/videos/your-stepdad-cant-even-satisfy-me-xhgHXG4", + "review": 0, + "should_download": 0, + "title": "Dein Stiefvater kann mich nicht einmal befriedigen | xHamster", + "file_name": "Dein Stiefvater kann mich nicht einmal befriedigen [xhgHXG4].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ae2e76ac-39ee-43b0-8321-badb9e4b0afa.mp4" + }, + { + "id": "ae732ddc-dd3f-414b-bd6b-f7d81a1d84a9", + "created_date": "2025-01-16 20:00:04.806579", + "last_modified_date": "2025-01-16 20:00:04.806585", + "version": 0, + "url": "https://ge.xhamster.com/videos/believe-it-or-not-real-step-sisters-dolly-leigh-and-izzy-lush-got-dumped-on-the-same-day-teamskeet-xhmuxO4", + "review": 0, + "should_download": 0, + "title": "Ob Sie es glauben oder nicht, echte stiefschwestern dolly leigh und Izzy lush wurden am selben tag entsorgt -teamSkeet | xHamster", + "file_name": "Ob Sie es glauben oder nicht, echte stiefschwestern dolly leigh und Izzy lush wurden am selben tag entsorgt -teamSkeet [xhmuxO4].mp4", + "path": null, + "cloud_link": "/data/media/ae732ddc-dd3f-414b-bd6b-f7d81a1d84a9.mp4" + }, + { + "id": "aebc3169-db9d-4fd7-a274-90333d2d1942", + "created_date": "2024-07-25 07:29:46.386998", + "last_modified_date": "2024-07-25 07:29:46.386998", + "version": 0, + "url": "https://ge.xhamster.com/videos/barely-legal-03-13858051", + "review": 0, + "should_download": 0, + "title": "Kaum legal # 03 | xHamster", + "file_name": "Kaum legal # 03 [13858051].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/aebc3169-db9d-4fd7-a274-90333d2d1942.mp4" + }, + { + "id": "aeddff77-6cf9-40f3-a505-eced98b071a4", + "created_date": "2024-07-25 07:29:44.444217", + "last_modified_date": "2024-07-25 07:29:44.444217", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsister-wants-to-play-truth-or-dare-e4-touch-my-cock-xhlV9iF", + "review": 0, + "should_download": 0, + "title": "Stiefschwester will Wahrheit oder Pflicht spielen e4: Ber\u00fchre meinen Schwanz | xHamster", + "file_name": "Stiefschwester will Wahrheit oder Pflicht spielen e4\uff1a Ber\u00fchre meinen Schwanz [xhlV9iF].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/aeddff77-6cf9-40f3-a505-eced98b071a4.mp4" + }, + { + "id": "aedef29a-2c48-4f51-9d7b-0220a1d50f81", + "created_date": "2024-07-25 07:29:47.085127", + "last_modified_date": "2024-07-25 07:29:47.085127", + "version": 0, + "url": "https://ge.xhamster.com/videos/heisse-schulmadchenluste-1984-with-anne-karna-9219899", + "review": 0, + "should_download": 0, + "title": "Heisse Schulmadchenluste (1984) mit Anne Karna | xHamster", + "file_name": "Heisse Schulmadchenluste (1984) mit Anne Karna [9219899].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/aedef29a-2c48-4f51-9d7b-0220a1d50f81.mp4" + }, + { + "id": "aef15d32-266d-4d8f-bd00-0a3ce84e3878", + "created_date": "2024-07-25 07:29:44.925513", + "last_modified_date": "2024-07-25 07:29:44.925513", + "version": 0, + "url": "https://ge.xhamster.com/videos/threesome-with-the-big-boobs-secretary-cara-st-germain-xhoXRVq", + "review": 0, + "should_download": 0, + "title": "Dreier mit der vollbusigen sekret\u00e4rin cara st germain | xHamster", + "file_name": "Dreier mit der vollbusigen sekret\u00e4rin cara st germain [xhoXRVq].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/aef15d32-266d-4d8f-bd00-0a3ce84e3878.mp4" + }, + { + "id": "af2b5bcc-983b-450b-8a50-98c74593cc94", + "created_date": "2024-07-25 07:29:47.047597", + "last_modified_date": "2024-07-25 07:29:47.047597", + "version": 0, + "url": "https://ge.xhamster.com/videos/outdoor-adventure-sassy-blonde-loves-masturbating-in-the-woods-so-random-strangers-can-catch-her-xhFqxWu", + "review": 0, + "should_download": 0, + "title": "Outdoor-abenteuer - eine freche blondine liebt es, im wald zu masturbieren, damit zuf\u00e4llige Fremde sie fangen k\u00f6nnen | xHamster", + "file_name": "Outdoor-abenteuer - eine freche blondine liebt es, im wald zu masturbieren, damit zuf\u00e4llige Fremde sie fangen k\u00f6nnen [xhFqxWu].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/af2b5bcc-983b-450b-8a50-98c74593cc94.mp4" + }, + { + "id": "af369219-7e10-4427-b030-d36a7f34a393", + "created_date": "2024-07-25 07:29:46.591033", + "last_modified_date": "2024-07-25 07:29:46.591033", + "version": 0, + "url": "https://ge.xhamster.com/videos/step-moms-bang-teens-naughty-needs-threesome-6470534", + "review": 0, + "should_download": 0, + "title": "Stiefmutter knallt Teenager - Frech braucht Dreier | xHamster", + "file_name": "Stiefmutter knallt Teenager - Frech braucht Dreier [6470534].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/af369219-7e10-4427-b030-d36a7f34a393.mp4" + }, + { + "id": "af6e8c6b-1243-4257-9adb-8d07af507bda", + "created_date": "2024-12-29 23:53:27.935353", + "last_modified_date": "2024-12-29 23:53:27.935353", + "version": 0, + "url": "https://ge.xhamster.com/videos/sweet-honey-14698921", + "review": 0, + "should_download": 0, + "title": "S\u00fc\u00dfer Schatz | xHamster", + "file_name": "S\u00fc\u00dfer Schatz [14698921].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/af6e8c6b-1243-4257-9adb-8d07af507bda.mp4" + }, + { + "id": "b0166a4d-0652-41d5-b428-03e0c7e14317", + "created_date": "2024-07-25 07:29:47.604382", + "last_modified_date": "2024-07-25 07:29:47.604382", + "version": 0, + "url": "https://ge.xhamster.com/videos/chrissy-the-campus-slut-1-xhuRxvd", + "review": 0, + "should_download": 0, + "title": "Chrissy, die Campus-Schlampe # 1 | xHamster", + "file_name": "Chrissy, die Campus-Schlampe # 1 [xhuRxvd].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b0166a4d-0652-41d5-b428-03e0c7e14317.mp4" + }, + { + "id": "b0293303-5492-4143-a802-1d6a171fc28b", + "created_date": "2024-07-25 07:29:47.028354", + "last_modified_date": "2025-01-03 11:56:37.648000", + "version": 1, + "url": "https://ge.xhamster.com/videos/my-boyfriends-dad-shared-his-wisdom-and-i-came-s30-e5-xh6jy53", + "review": 0, + "should_download": 0, + "title": "Der vater meines freundes teilte seine Weisheit und ich kam - S30: E5 | xHamster", + "file_name": "Der vater meines freundes teilte seine Weisheit und ich kam - S30\uff1a E5 [xh6jy53].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b0293303-5492-4143-a802-1d6a171fc28b.mp4" + }, + { + "id": "b0791e4b-a4d2-434f-b2a7-e1b25bac1dc8", + "created_date": "2024-10-21 15:08:43.556218", + "last_modified_date": "2024-10-21 16:31:35.231000", + "version": 1, + "url": "https://ge.xhamster.com/videos/fickparty-und-wildes-treiben-xh7cWy4", + "review": 0, + "should_download": 0, + "title": "Fickparty Und Wildes Treiben, Free Tight Pussy HD Porn 59 | xHamster", + "file_name": "Fickparty und wildes treiben [xh7cWy4].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/b0791e4b-a4d2-434f-b2a7-e1b25bac1dc8.mp4" + }, + { + "id": "b082f26b-f7ce-42bb-a15e-e13ea04ff67a", + "created_date": "2024-07-25 07:29:45.800483", + "last_modified_date": "2024-07-25 07:29:45.800483", + "version": 0, + "url": "https://ge.xhamster.com/videos/public-beach-fucking-on-caribbean-beach-blowjob-public-sex-xhXvUGQ", + "review": 0, + "should_download": 0, + "title": "\u00d6ffentlicher Strandfick am Karibikstrand, Blowjob, Sex in der \u00d6ffentlichkeit | xHamster", + "file_name": "\u00d6ffentlicher Strandfick am Karibikstrand, Blowjob, Sex in der \u00d6ffentlichkeit [xhXvUGQ].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b082f26b-f7ce-42bb-a15e-e13ea04ff67a.mp4" + }, + { + "id": "b08e0677-e616-46ed-b603-0210757c8836", + "created_date": "2024-10-07 20:47:56.418609", + "last_modified_date": "2024-10-21 16:31:38.109000", + "version": 1, + "url": "https://ge.xhamster.com/videos/18-videoz-unplanned-threesome-6988354", + "review": 0, + "should_download": 0, + "title": "18 Videoz - ungeplanter Dreier | xHamster", + "file_name": "18 Videoz - ungeplanter Dreier [6988354].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/b08e0677-e616-46ed-b603-0210757c8836.mp4" + }, + { + "id": "b0bfd9c0-c7e5-4689-8192-fae7ce1c8c7c", + "created_date": "2024-08-09 19:03:49.366496", + "last_modified_date": "2024-08-09 19:03:49.366496", + "version": 0, + "url": "https://ge.xhamster.com/videos/fucking-with-friends-xh088HJ", + "review": 0, + "should_download": 0, + "title": "Ficken mit Freunden | xHamster", + "file_name": "Ficken mit Freunden [xh088HJ].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/b0bfd9c0-c7e5-4689-8192-fae7ce1c8c7c.mp4" + }, + { + "id": "b1210e3c-263d-4fd0-a1dd-8b612a4e5cdf", + "created_date": "2024-07-25 07:29:44.370804", + "last_modified_date": "2024-07-25 07:29:44.370804", + "version": 0, + "url": "https://ge.xhamster.com/videos/hes-much-bigger-then-my-husband-xho6pH1", + "review": 0, + "should_download": 0, + "title": "Er ist viel gr\u00f6\u00dfer als mein Ehemann !. | xHamster", + "file_name": "Er ist viel gr\u00f6\u00dfer als mein Ehemann !. [xho6pH1].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b1210e3c-263d-4fd0-a1dd-8b612a4e5cdf.mp4" + }, + { + "id": "b12cbfa0-ac9c-4c95-8e24-3685ab04d0d0", + "created_date": "2024-07-25 07:29:46.933293", + "last_modified_date": "2024-07-25 07:29:46.933293", + "version": 0, + "url": "https://ge.xhamster.com/videos/im-swingerclub-mich-vom-fremden-mann-bumsen-lassen-xhaT7H1", + "review": 0, + "should_download": 0, + "title": "Im Swingerclub Mich Vom Fremden Mann Bumsen Lassen: Porn ad | xHamster", + "file_name": "Im Swingerclub mich vom fremden Mann bumsen lassen [xhaT7H1].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b12cbfa0-ac9c-4c95-8e24-3685ab04d0d0.mp4" + }, + { + "id": "b19fa638-0c01-426e-80f3-23c2cac18b40", + "created_date": "2024-07-25 07:29:47.318396", + "last_modified_date": "2024-07-25 07:29:47.318396", + "version": 0, + "url": "https://ge.xhamster.com/videos/pool-party-2-guys-and-5-czech-girls-teenrs-com-7853858", + "review": 0, + "should_download": 0, + "title": "Poolparty 2 Typen und 5 tschechische M\u00e4dchen teenrs.com | xHamster", + "file_name": "Poolparty 2 Typen und 5 tschechische M\u00e4dchen teenrs.com [7853858].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b19fa638-0c01-426e-80f3-23c2cac18b40.mp4" + }, + { + "id": "b1b26c98-86e0-479e-82d3-a6a310f05732", + "created_date": "2024-07-25 07:29:46.899856", + "last_modified_date": "2024-07-25 07:29:46.899856", + "version": 0, + "url": "https://ge.xhamster.com/videos/teenies-gerade-18-bildhuebsch-und-sehr-versaut-7955728", + "review": 0, + "should_download": 0, + "title": "Teenies Gerade 18 - Bildhuebsch Und Sehr Versaut: Porn ff | xHamster", + "file_name": "Teenies gerade 18 - Bildhuebsch und sehr versaut [7955728].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b1b26c98-86e0-479e-82d3-a6a310f05732.mp4" + }, + { + "id": "b1cbcd8d-a9d7-4c84-8179-89254432a1f6", + "created_date": "2024-07-25 07:29:46.173453", + "last_modified_date": "2024-07-25 07:29:46.173453", + "version": 0, + "url": "https://ge.xhamster.com/videos/orgy-1978-11901295", + "review": 0, + "should_download": 0, + "title": "Orgie (1978) | xHamster", + "file_name": "Orgie (1978) [11901295].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b1cbcd8d-a9d7-4c84-8179-89254432a1f6.mp4" + }, + { + "id": "b1e9463e-9324-4efd-8f16-7700d867994e", + "created_date": "2024-07-25 07:29:44.620810", + "last_modified_date": "2024-07-25 07:29:44.620810", + "version": 0, + "url": "https://ge.xhamster.com/videos/verbotenes-spiel-1979-6317174", + "review": 0, + "should_download": 0, + "title": "Verbotenes Spiel 1979, Free Orgy Porn Video 63 | xHamster", + "file_name": "Verbotenes Spiel (1979) [6317174].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b1e9463e-9324-4efd-8f16-7700d867994e.mp4" + }, + { + "id": "b1eab02f-29f2-40ca-bb07-04f067124813", + "created_date": "2024-07-25 07:29:44.399820", + "last_modified_date": "2024-07-25 07:29:44.399820", + "version": 0, + "url": "https://ge.xhamster.com/videos/wife-with-friends-1643137", + "review": 0, + "should_download": 0, + "title": "Ehefrau mit Freunden | xHamster", + "file_name": "Ehefrau mit Freunden [1643137].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b1eab02f-29f2-40ca-bb07-04f067124813.mp4" + }, + { + "id": "b20d1361-9925-42aa-9d49-4cb5584f493c", + "created_date": "2024-12-29 23:53:27.885234", + "last_modified_date": "2024-12-29 23:53:27.885234", + "version": 0, + "url": "https://ge.xhamster.com/videos/meine-geile-weihnachtsfeier-episode-4-xhEl6M2", + "review": 0, + "should_download": 0, + "title": "Meine Geile Weihnachtsfeier - Episode 4, Porn 98 | xHamster", + "file_name": "Meine geile weihnachtsfeier - Episode 4 [xhEl6M2].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/b20d1361-9925-42aa-9d49-4cb5584f493c.mp4" + }, + { + "id": "b22ae561-61f9-44c4-af1c-62201baf0c7e", + "created_date": "2024-07-25 07:29:46.030151", + "last_modified_date": "2024-07-25 07:29:46.030151", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-first-threesome-s9-e7-xh7AtHf", + "review": 0, + "should_download": 0, + "title": "Mein erster dreier - s9: e7 | xHamster", + "file_name": "Mein erster dreier - s9\uff1a e7 [xh7AtHf].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b22ae561-61f9-44c4-af1c-62201baf0c7e.mp4" + }, + { + "id": "b2755db1-3727-42f7-9552-1352f5786aaf", + "created_date": "2024-07-25 07:29:46.362247", + "last_modified_date": "2024-07-25 07:29:46.362247", + "version": 0, + "url": "https://ge.xhamster.com/videos/wedding-party-297919", + "review": 0, + "should_download": 0, + "title": "Hochzeitsfeier | xHamster", + "file_name": "Hochzeitsfeier [297919].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b2755db1-3727-42f7-9552-1352f5786aaf.mp4" + }, + { + "id": "b2ab263e-a5fd-40f0-aeec-d3ca8f4e3c68", + "created_date": "2024-11-10 16:53:33.487704", + "last_modified_date": "2024-11-10 16:53:33.487704", + "version": 0, + "url": "https://ge.xhamster.com/videos/miriam-teacher-14218585", + "review": 0, + "should_download": 0, + "title": "Miriam - Lehrerin | xHamster", + "file_name": "Miriam - Lehrerin [14218585].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/b2ab263e-a5fd-40f0-aeec-d3ca8f4e3c68.mp4" + }, + { + "id": "b2f300a0-391b-4a59-8d04-23642f67d43a", + "created_date": "2024-07-25 07:29:47.907080", + "last_modified_date": "2024-07-25 07:29:47.907080", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsisters-bff-says-i-dare-you-to-get-brother-to-show-us-his-dick-xhopwLb", + "review": 0, + "should_download": 0, + "title": "Die Freundin der Stiefschwester sagt, ich wage es, dass du Stiefbruder bekommst, der uns seinen Schwanz zeigt | xHamster", + "file_name": "Die Freundin der Stiefschwester sagt, ich wage es, dass du Stiefbruder bekommst, der uns seinen Schwanz zeigt [xhopwLb].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b2f300a0-391b-4a59-8d04-23642f67d43a.mp4" + }, + { + "id": "b31acd3b-535f-413e-88e5-807780c19c1d", + "created_date": "2024-08-09 19:04:01.562661", + "last_modified_date": "2024-08-09 19:04:01.562661", + "version": 0, + "url": "https://ge.xhamster.com/videos/performance-review-girl-gets-wet-when-boss-fucks-client-xhUXamo", + "review": 0, + "should_download": 0, + "title": "Performance-Review - M\u00e4dchen wird nass, wenn der Chef Kunden fickt | xHamster", + "file_name": "Performance-Review - M\u00e4dchen wird nass, wenn der Chef Kunden fickt [xhUXamo].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/b31acd3b-535f-413e-88e5-807780c19c1d.mp4" + }, + { + "id": "b31d0368-7367-4b7d-81d9-5650e6794d2d", + "created_date": "2024-07-25 07:29:47.427028", + "last_modified_date": "2024-07-25 07:29:47.427028", + "version": 0, + "url": "https://ge.xhamster.com/videos/babes-step-mom-lessons-alexa-tomas-and-cindy-loarn-and-g-8409763", + "review": 0, + "should_download": 0, + "title": "Babes - Stiefmutter-Unterricht - Alexa Tomas und Cindy Loarn und g | xHamster", + "file_name": "Babes - Stiefmutter-Unterricht - Alexa Tomas und Cindy Loarn und g [8409763].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b31d0368-7367-4b7d-81d9-5650e6794d2d.mp4" + }, + { + "id": "b331cb7d-764b-4e36-adaf-3d06ae31098a", + "created_date": "2024-07-25 07:29:44.985492", + "last_modified_date": "2024-07-25 07:29:44.985492", + "version": 0, + "url": "https://ge.xhamster.com/videos/your-panties-are-on-the-floor-what-were-you-guys-just-doing-mandy-waters-asks-dani-diaz-s6-e9-xhUNfON", + "review": 0, + "should_download": 0, + "title": "\"Dein h\u00f6schen ist auf dem Boden. Was haben Sie jungs gerade gemacht?\" Mandy Waters fragt Dani Diaz -S6: E9 | xHamster", + "file_name": "\uff02Dein h\u00f6schen ist auf dem Boden. Was haben Sie jungs gerade gemacht\uff1f\uff02 Mandy Waters fragt Dani Diaz -S6\uff1a E9 [xhUNfON].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b331cb7d-764b-4e36-adaf-3d06ae31098a.mp4" + }, + { + "id": "b339110d-bdf8-482d-9239-06ebc3184077", + "created_date": "2024-11-01 21:08:26.932520", + "last_modified_date": "2024-11-01 21:08:26.932520", + "version": 0, + "url": "https://ge.xhamster.com/videos/german-vintage-5088343", + "review": 0, + "should_download": 0, + "title": "Deutscher Retro | xHamster", + "file_name": "Deutscher Retro [5088343].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/b339110d-bdf8-482d-9239-06ebc3184077.mp4" + }, + { + "id": "b33dc0b1-5a82-41c7-ac4e-09085464d6fc", + "created_date": "2024-12-29 23:53:27.928213", + "last_modified_date": "2024-12-29 23:53:27.928213", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-enjoy-evening-at-home-xhTkwhu", + "review": 0, + "should_download": 0, + "title": "Familie genie\u00dft den Abend zu Hause | xHamster", + "file_name": "Familie genie\u00dft den Abend zu Hause [xhTkwhu].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/b33dc0b1-5a82-41c7-ac4e-09085464d6fc.mp4" + }, + { + "id": "b39a46e0-3f13-405a-a9a8-54e8526661df", + "created_date": "2024-07-25 07:29:45.203500", + "last_modified_date": "2024-07-25 07:29:45.203500", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-story-of-how-i-was-caught-in-bed-with-a-sex-machine-xhGmJYn", + "review": 0, + "should_download": 0, + "title": "Die Geschichte davon, wie ich mit einer sexmaschine im bett erwischt wurde. | xHamster", + "file_name": "Die Geschichte davon, wie ich mit einer sexmaschine im bett erwischt wurde. [xhGmJYn].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b39a46e0-3f13-405a-a9a8-54e8526661df.mp4" + }, + { + "id": "b3c29573-d7b6-4c80-a384-b3eea5296001", + "created_date": "2024-07-25 07:29:47.171718", + "last_modified_date": "2024-07-25 07:29:47.171718", + "version": 0, + "url": "https://ge.xhamster.com/videos/hot-teenage-assets-1978-10059593", + "review": 0, + "should_download": 0, + "title": "Hei\u00dfe Teenager-Assets (1978) | xHamster", + "file_name": "Hei\u00dfe Teenager-Assets (1978) [10059593].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b3c29573-d7b6-4c80-a384-b3eea5296001.mp4" + }, + { + "id": "b46b3122-47c7-4f16-97b1-781130e84afe", + "created_date": "2024-07-25 07:29:45.445966", + "last_modified_date": "2024-07-25 07:29:45.445966", + "version": 0, + "url": "https://ge.xhamster.com/videos/babes-step-mom-lessons-leny-ewil-gina-gerson-kathia-no-8563379", + "review": 0, + "should_download": 0, + "title": "Sch\u00e4tzchen - Stiefmutterunterricht - Leny Ewil, Gina Gerson, Kathia No | xHamster", + "file_name": "Sch\u00e4tzchen - Stiefmutterunterricht - Leny Ewil, Gina Gerson, Kathia No [8563379].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b46b3122-47c7-4f16-97b1-781130e84afe.mp4" + }, + { + "id": "b476d3d6-c511-41e9-8ffc-e1da33b66482", + "created_date": "2024-07-25 07:29:44.688498", + "last_modified_date": "2024-07-25 07:29:44.688498", + "version": 0, + "url": "https://ge.xhamster.com/videos/kendra-spade-caught-wetting-herself-by-stepdad-xhMmVh5", + "review": 0, + "should_download": 0, + "title": "Kendra Spade erwischte sich, als sie sich von Stiefvater benetzte | xHamster", + "file_name": "Kendra Spade erwischte sich, als sie sich von Stiefvater benetzte [xhMmVh5].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b476d3d6-c511-41e9-8ffc-e1da33b66482.mp4" + }, + { + "id": "b48d26a8-4ae1-43b8-9d66-001a759ea69b", + "created_date": "2024-08-09 19:05:14.504690", + "last_modified_date": "2024-08-09 19:05:14.504690", + "version": 0, + "url": "https://ge.xhamster.com/videos/topless-chicks-in-a-pool-with-horny-as-fuck-college-frat-guys-tiny-hole-babe-mia-gold-gets-nailed-by-two-dicks-xhlVpld", + "review": 0, + "should_download": 0, + "title": "Topless Chicks in a Pool with Horny as Fuck College Frat Guys Tiny Hole Babe Mia Gold gets Nailed by Two Dicks | xHamster", + "file_name": "topless chicks in a pool with horny as fuck college frat guys Tiny Hole Babe Mia Gold Gets Nailed by Two Dicks [xhlVpld].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/b48d26a8-4ae1-43b8-9d66-001a759ea69b.mp4" + }, + { + "id": "b48f6dbd-714c-4763-baa8-5c215b4a8fc9", + "created_date": "2024-07-25 07:29:47.247437", + "last_modified_date": "2024-07-25 07:29:47.247437", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-inc-1-full-german-movie-xhBoAf5", + "review": 0, + "should_download": 0, + "title": "Familie inkl. 1 - kompletter deutscher Film | xHamster", + "file_name": "Familie inkl. 1 - kompletter deutscher Film [xhBoAf5].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b48f6dbd-714c-4763-baa8-5c215b4a8fc9.mp4" + }, + { + "id": "b5084dde-0cb2-4055-8a26-cb87e4791303", + "created_date": "2024-07-25 07:29:47.877653", + "last_modified_date": "2024-07-25 07:29:47.877653", + "version": 0, + "url": "https://ge.xhamster.com/videos/chateau-duval-full-original-movie-in-hd-xhAnMFs", + "review": 0, + "should_download": 0, + "title": "Chateau Duval - (kompletter originalfilm in HD) | xHamster", + "file_name": "Chateau Duval - (kompletter originalfilm in HD) [xhAnMFs].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b5084dde-0cb2-4055-8a26-cb87e4791303.mp4" + }, + { + "id": "b50a7f17-b2f6-464b-ba0d-e401b0658d58", + "created_date": "2024-07-25 07:29:48.025482", + "last_modified_date": "2024-07-25 07:29:48.025482", + "version": 0, + "url": "https://ge.xhamster.com/videos/brunette-keeps-all-the-three-cocks-engage-throughout-the-foursome-session-5511018", + "review": 0, + "should_download": 0, + "title": "Br\u00fcnette h\u00e4lt alle drei Schw\u00e4nze w\u00e4hrend der Vierer-Session in Eingriff | xHamster", + "file_name": "Br\u00fcnette h\u00e4lt alle drei Schw\u00e4nze w\u00e4hrend der Vierer-Session in Eingriff [5511018].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b50a7f17-b2f6-464b-ba0d-e401b0658d58.mp4" + }, + { + "id": "b518a416-ee6c-46d0-8bc8-710dd2324910", + "created_date": "2024-07-25 07:29:47.544510", + "last_modified_date": "2024-07-25 07:29:47.544510", + "version": 0, + "url": "https://ge.xhamster.com/videos/junger-ist-enger-ganzer-film-xhdNW8X", + "review": 0, + "should_download": 0, + "title": "Junger Ist Enger Ganzer Film, Free Romantic HD Porn 62 | xHamster", + "file_name": "Junger ist enger (GANZER FILM) [xhdNW8X].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b518a416-ee6c-46d0-8bc8-710dd2324910.mp4" + }, + { + "id": "b51a2ded-0da6-4006-a2f4-d34765f40ac8", + "created_date": "2024-11-10 16:53:33.480594", + "last_modified_date": "2024-11-10 16:53:33.480594", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-young-like-it-hot-1983-us-full-movie-bdrip-xhViCnc", + "review": 0, + "should_download": 0, + "title": "Die Jungen m\u00f6gen es hei\u00df (1983, us, kompletter Film, bdrip) | xHamster", + "file_name": "Die Jungen m\u00f6gen es hei\u00df (1983, us, kompletter Film, bdrip) [xhViCnc].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/b51a2ded-0da6-4006-a2f4-d34765f40ac8.mp4" + }, + { + "id": "b569fa40-68cf-411d-bf99-052bb79da277", + "created_date": "2024-07-25 07:29:45.479895", + "last_modified_date": "2024-07-25 07:29:45.479895", + "version": 0, + "url": "https://ge.xhamster.com/videos/massage-therapist-redhead-cheats-xhydjoy", + "review": 0, + "should_download": 0, + "title": "Rothaarige Masseurin betr\u00fcgt | xHamster", + "file_name": "Rothaarige Masseurin betr\u00fcgt [xhydjoy].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b569fa40-68cf-411d-bf99-052bb79da277.mp4" + }, + { + "id": "b56a0048-e384-4601-8a47-0b6037162f01", + "created_date": "2024-07-25 07:29:45.824952", + "last_modified_date": "2024-07-25 07:29:45.824952", + "version": 0, + "url": "https://ge.xhamster.com/videos/classic-1980-verbotene-gelueste-part-2-xhpFde0", + "review": 0, + "should_download": 0, + "title": "Classic 1980 - Verbotene Gelueste Part 2, Porn de | xHamster", + "file_name": "Classic 1980 - Verbotene Gelueste part 2 [xhpFde0].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b56a0048-e384-4601-8a47-0b6037162f01.mp4" + }, + { + "id": "b572b5a2-358c-4337-82af-1a9a6ba28e80", + "created_date": "2024-07-25 07:29:45.067515", + "last_modified_date": "2024-07-25 07:29:45.067515", + "version": 0, + "url": "https://ge.xhamster.com/videos/big-naturals-teen-enjoys-bbc-outdoors-13070001", + "review": 0, + "should_download": 0, + "title": "Gro\u00dfes nat\u00fcrliches Teen genie\u00dft BBC im Freien | xHamster", + "file_name": "Gro\u00dfes nat\u00fcrliches Teen genie\u00dft BBC im Freien [13070001].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b572b5a2-358c-4337-82af-1a9a6ba28e80.mp4" + }, + { + "id": "b577b017-8003-44ce-9c44-ce4fcd1f6ed5", + "created_date": "2024-07-25 07:29:44.798031", + "last_modified_date": "2024-07-25 07:29:44.798031", + "version": 0, + "url": "https://ge.xhamster.com/videos/adult-time-hotwife-casey-calverts-horny-in-laws-airtight-gangbang-bukkake-her-with-husbands-help-xhn4yql", + "review": 0, + "should_download": 0, + "title": "Adult Time - hotwife casey Calverts geile schwiedermutter, luftige gangbang und bukkake sie mit der hilfe des ehemanns | xHamster", + "file_name": "Adult Time - hotwife casey Calverts geile schwiedermutter, luftige gangbang und bukkake sie mit der hilfe des ehemanns [xhn4yql].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b577b017-8003-44ce-9c44-ce4fcd1f6ed5.mp4" + }, + { + "id": "b5907364-873a-4a77-90cf-fd7d483e1c10", + "created_date": "2024-07-25 07:29:46.102422", + "last_modified_date": "2024-07-25 07:29:46.102422", + "version": 0, + "url": "https://ge.xhamster.com/videos/busty-milf-with-natural-tits-fucks-on-the-beach-11296164", + "review": 0, + "should_download": 0, + "title": "Vollbusige MILF mit nat\u00fcrlichen Titten fickt am Strand | xHamster", + "file_name": "Vollbusige MILF mit nat\u00fcrlichen Titten fickt am Strand [11296164].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b5907364-873a-4a77-90cf-fd7d483e1c10.mp4" + }, + { + "id": "b5ac95d3-ece2-4c05-981c-88ae86ec7fba", + "created_date": "2024-07-25 07:29:45.119817", + "last_modified_date": "2024-07-25 07:29:45.119817", + "version": 0, + "url": "https://ge.xhamster.com/videos/orgy-with-lots-of-cumshots-802168", + "review": 0, + "should_download": 0, + "title": "Orgie mit vielen Cumshots | xHamster", + "file_name": "Orgie mit vielen Cumshots [802168].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b5ac95d3-ece2-4c05-981c-88ae86ec7fba.mp4" + }, + { + "id": "b5ceb5af-2318-48e7-9c83-aec81a0ff0e2", + "created_date": "2024-11-10 16:53:33.496795", + "last_modified_date": "2024-11-10 16:53:33.496795", + "version": 0, + "url": "https://ge.xhamster.com/videos/intimacy-vol-2-full-original-movie-xh0omub", + "review": 0, + "should_download": 0, + "title": "Intimacy vol.2 (kompletter Originalfilm) | xHamster", + "file_name": "Intimacy vol.2 (kompletter Originalfilm) [xh0omub].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/b5ceb5af-2318-48e7-9c83-aec81a0ff0e2.mp4" + }, + { + "id": "b6075589-21ce-4ea1-8738-c1854d5d9122", + "created_date": "2025-01-19 13:42:39.706398", + "last_modified_date": "2025-01-19 13:42:39.706404", + "version": 0, + "url": "https://ge.xhamster.com/videos/step-mom-and-step-aunt-want-to-get-pregnant-at-the-same-time-by-fucking-stepson-together-xhrMvBr", + "review": 0, + "should_download": 0, + "title": "Stiefmutter und stieftante wollen gleichzeitig schwanger werden, indem sie zusammen stiefsohn ficken | xHamster", + "file_name": "Stiefmutter und stieftante wollen gleichzeitig schwanger werden, indem sie zusammen stiefsohn ficken [xhrMvBr].mp4", + "path": null, + "cloud_link": null + }, + { + "id": "b62eb2f4-113b-430c-881b-0dbce5031d8d", + "created_date": "2024-07-25 07:29:44.381480", + "last_modified_date": "2024-07-25 07:29:44.381480", + "version": 0, + "url": "https://ge.xhamster.com/videos/mydirtyhobby-sauna-hot-threesome-xh45mq7", + "review": 0, + "should_download": 0, + "title": "Mydirtyhobby - hei\u00dfer Dreier in der Sauna | xHamster", + "file_name": "Mydirtyhobby - hei\u00dfer Dreier in der Sauna [xh45mq7].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/b62eb2f4-113b-430c-881b-0dbce5031d8d.mp4" + }, + { + "id": "b63149eb-b41b-457e-b973-46ac84f5a40e", + "created_date": "2025-01-16 19:59:37.074074", + "last_modified_date": "2025-01-16 19:59:37.074080", + "version": 0, + "url": "https://ge.xhamster.com/videos/orgies-were-different-back-then-xhVLdmF", + "review": 0, + "should_download": 0, + "title": "Orgien waren damals anders | xHamster", + "file_name": "Orgien waren damals anders [xhVLdmF].mp4", + "path": null, + "cloud_link": "/data/media/b63149eb-b41b-457e-b973-46ac84f5a40e.mp4" + }, + { + "id": "b6ce820b-5401-46ae-9aed-b0cbe332f4f5", + "created_date": "2024-08-09 20:40:06.114521", + "last_modified_date": "2024-08-16 10:31:31.551000", + "version": 1, + "url": "https://ge.xhamster.com/videos/step-son-step-mom-fuck-around-with-step-cousin-and-stepaunt-summer-vacation-taboo-family-orgy-xhu12nz", + "review": 0, + "should_download": 0, + "title": "Stiefsohn & stiefmutter ficken mit stiefcousin und stieftante - sommerferien tabu familienorgie | xHamster", + "file_name": "Stiefsohn & stiefmutter ficken mit stiefcousin und stieftante - sommerferien tabu familienorgie [xhu12nz].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/b6ce820b-5401-46ae-9aed-b0cbe332f4f5.mp4" + }, + { + "id": "b6cf9fba-a589-4cef-b380-4a49633f9e21", + "created_date": "2024-07-25 07:29:46.137338", + "last_modified_date": "2024-07-25 07:29:46.137338", + "version": 0, + "url": "https://ge.xhamster.com/videos/seafaring-teen-facialized-after-foursome-boat-banging-10295098", + "review": 0, + "should_download": 0, + "title": "Seafaring Teen nach dem Vierer-Boot-H\u00e4mmern ins Gesicht gespritzt | xHamster", + "file_name": "Seafaring Teen nach dem Vierer-Boot-H\u00e4mmern ins Gesicht gespritzt [10295098].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b6cf9fba-a589-4cef-b380-4a49633f9e21.mp4" + }, + { + "id": "b70aa94b-66bf-4777-bfc8-36efb530b874", + "created_date": "2024-07-25 07:29:44.273751", + "last_modified_date": "2024-07-25 07:29:44.273751", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-pervers-1997-xhWlXY8", + "review": 0, + "should_download": 0, + "title": "Perverse familie (1997) | xHamster", + "file_name": "Perverse familie (1997) [xhWlXY8].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/b70aa94b-66bf-4777-bfc8-36efb530b874.mp4" + }, + { + "id": "b769d481-0c54-41e8-a263-85aae2789fef", + "created_date": "2024-07-25 07:29:45.941196", + "last_modified_date": "2024-07-25 07:29:45.941196", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-taboo-german-wilder-fick-mit-notgeiler-verwandtschaft-xhD3DhB", + "review": 0, + "should_download": 0, + "title": "Family Taboo German - Wilder Fick Mit Notgeiler Verwandtschaft | xHamster", + "file_name": "FAMILY TABOO GERMAN - Wilder fick mit notgeiler Verwandtschaft [xhD3DhB].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b769d481-0c54-41e8-a263-85aae2789fef.mp4" + }, + { + "id": "b76d3793-ce72-4ed5-9eb8-c53f28dec4c2", + "created_date": "2024-07-25 07:29:45.274435", + "last_modified_date": "2024-07-25 07:29:45.274435", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsiblingscaught-tricking-step-sis-into-a-sexual-treat-12694513", + "review": 0, + "should_download": 0, + "title": "Stepsiblingscaught - Stiefschwester zu einer sexuellen Belohnung tricksen | xHamster", + "file_name": "Stepsiblingscaught - Stiefschwester zu einer sexuellen Belohnung tricksen [12694513].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b76d3793-ce72-4ed5-9eb8-c53f28dec4c2.mp4" + }, + { + "id": "b781a759-92d3-4ba1-bcc4-a0f0c8099f8e", + "created_date": "2024-07-25 07:29:46.722385", + "last_modified_date": "2024-07-25 07:29:46.722385", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-hit-1975-11241108", + "review": 0, + "should_download": 0, + "title": "Der Hit (1975) | xHamster", + "file_name": "Der Hit (1975) [11241108].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b781a759-92d3-4ba1-bcc4-a0f0c8099f8e.mp4" + }, + { + "id": "b78f9f1a-8848-495d-bb88-77a93427f7db", + "created_date": "2024-07-25 07:29:46.942136", + "last_modified_date": "2024-07-25 07:29:46.942136", + "version": 0, + "url": "https://ge.xhamster.com/videos/teen-with-ponytails-gets-aroused-from-touching-herself-xhap1aV", + "review": 0, + "should_download": 0, + "title": "Teen mit Pferdeschw\u00e4nzen wird erregt, sich selbst zu ber\u00fchren | xHamster", + "file_name": "Teen mit Pferdeschw\u00e4nzen wird erregt, sich selbst zu ber\u00fchren [xhap1aV].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b78f9f1a-8848-495d-bb88-77a93427f7db.mp4" + }, + { + "id": "b7950486-af5b-4233-a93a-15645144dd76", + "created_date": "2024-07-25 07:29:47.186747", + "last_modified_date": "2024-07-25 07:29:47.186747", + "version": 0, + "url": "https://ge.xhamster.com/videos/i-accidentally-sent-my-stepbrother-nudes-xhctN9H", + "review": 0, + "should_download": 0, + "title": "Ich habe versehentlich meinem Stiefbruder Nacktfotos geschickt | xHamster", + "file_name": "Ich habe versehentlich meinem Stiefbruder Nacktfotos geschickt [xhctN9H].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b7950486-af5b-4233-a93a-15645144dd76.mp4" + }, + { + "id": "b7aa45e4-0b5f-4c34-a18f-33a34b877940", + "created_date": "2024-07-25 07:29:46.522749", + "last_modified_date": "2024-07-25 07:29:46.522749", + "version": 0, + "url": "https://ge.xhamster.com/videos/teacher-fucked-by-two-students-2776488", + "review": 0, + "should_download": 0, + "title": "Lehrerin von zwei Sch\u00fclern gefickt | xHamster", + "file_name": "Lehrerin von zwei Sch\u00fclern gefickt [2776488].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b7aa45e4-0b5f-4c34-a18f-33a34b877940.mp4" + }, + { + "id": "b7c661de-949e-414b-af28-696668755ab7", + "created_date": "2024-07-25 07:29:47.476982", + "last_modified_date": "2024-07-25 07:29:47.476982", + "version": 0, + "url": "https://ge.xhamster.com/videos/eine-verdammt-heisse-braut-2-1989-9356438", + "review": 0, + "should_download": 0, + "title": "Eine Verdammt Heisse Braut 2 1989, Free Porn 5e | xHamster", + "file_name": "Eine verdammt heisse Braut 2 (1989) [9356438].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b7c661de-949e-414b-af28-696668755ab7.mp4" + }, + { + "id": "b7d36806-d8b8-49a5-95bb-7d8bb9200ecb", + "created_date": "2024-07-25 07:29:44.296342", + "last_modified_date": "2024-07-25 07:29:44.296342", + "version": 0, + "url": "https://ge.xhamster.com/videos/extreme-luck-1991-xhyvum3", + "review": 0, + "should_download": 0, + "title": "- Extreme Luck 1991: In German Porn Video 2d | xHamster", + "file_name": "- Extreme Luck (1991) [xhyvum3].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b7d36806-d8b8-49a5-95bb-7d8bb9200ecb.mp4" + }, + { + "id": "b7dc8b17-0b0f-4b4f-90ae-581f528edf1e", + "created_date": "2024-12-29 23:53:27.900216", + "last_modified_date": "2024-12-29 23:53:27.900216", + "version": 0, + "url": "https://ge.xhamster.com/videos/creampie-for-his-young-stepsis-14605707", + "review": 0, + "should_download": 0, + "title": "Creampie f\u00fcr seine junge Stiefschwester | xHamster", + "file_name": "Creampie f\u00fcr seine junge Stiefschwester [14605707].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/b7dc8b17-0b0f-4b4f-90ae-581f528edf1e.mp4" + }, + { + "id": "b7e9854d-5d76-4525-8fe5-1447312c1b48", + "created_date": "2024-07-25 07:29:47.137774", + "last_modified_date": "2024-07-25 07:29:47.137774", + "version": 0, + "url": "https://ge.xhamster.com/videos/vintage-german-virgin-maid-8509090", + "review": 0, + "should_download": 0, + "title": "Retro deutsches Retro Zimmerm\u00e4dchen | xHamster", + "file_name": "Retro deutsches Retro Zimmerm\u00e4dchen [8509090].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b7e9854d-5d76-4525-8fe5-1447312c1b48.mp4" + }, + { + "id": "b8363db8-8f4a-49bd-aa5e-873657a4d8a9", + "created_date": "2024-12-29 23:53:27.899146", + "last_modified_date": "2024-12-29 23:53:27.899146", + "version": 0, + "url": "https://ge.xhamster.com/videos/orgy-13634510", + "review": 0, + "should_download": 0, + "title": "Orgie | xHamster", + "file_name": "Orgie [13634510].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/b8363db8-8f4a-49bd-aa5e-873657a4d8a9.mp4" + }, + { + "id": "b8737f99-8c45-4d30-bbb7-c3477e90162e", + "created_date": "2024-07-25 07:29:47.945971", + "last_modified_date": "2024-07-25 07:29:47.945971", + "version": 0, + "url": "https://ge.xhamster.com/videos/orgien-auf-schloss-pimmelhof-1990s-german-sound-full-dvd-xhIcqJb", + "review": 0, + "should_download": 0, + "title": "Orgien Auf Schloss Pimmelhof 1990s German Sound Full Dvd | xHamster", + "file_name": "Orgien auf Schloss Pimmelhof (1990s, German sound, full DVD) [xhIcqJb].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b8737f99-8c45-4d30-bbb7-c3477e90162e.mp4" + }, + { + "id": "b8ae4604-ea5f-4236-ab0d-75c8b3f22a43", + "created_date": "2024-07-25 07:29:47.299815", + "last_modified_date": "2024-07-25 07:29:47.299815", + "version": 0, + "url": "https://ge.xhamster.com/videos/want-to-fuck-your-step-aunt-in-ass-family-fantasy-xhkN74e", + "review": 0, + "should_download": 0, + "title": "willst du deine stieftante in den arsch ficken? - Familienfantasie | xHamster", + "file_name": "willst du deine stieftante in den arsch ficken\uff1f - Familienfantasie [xhkN74e].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b8ae4604-ea5f-4236-ab0d-75c8b3f22a43.mp4" + }, + { + "id": "b8f54c89-e185-4fde-bf70-1f0c147a245f", + "created_date": "2024-07-25 07:29:46.277815", + "last_modified_date": "2024-07-25 07:29:46.277815", + "version": 0, + "url": "https://ge.xhamster.com/videos/fucking-mother-in-law-is-fun-xhtXUXG", + "review": 0, + "should_download": 0, + "title": "SCHWIEGERMUTTER FICKEN MACHT SPASS !!! | xHamster", + "file_name": "SCHWIEGERMUTTER FICKEN MACHT SPASS !!! [xhtXUXG].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b8f54c89-e185-4fde-bf70-1f0c147a245f.mp4" + }, + { + "id": "b9284d00-2673-4d03-9a16-923f14d30394", + "created_date": "2024-12-29 23:53:27.958369", + "last_modified_date": "2024-12-29 23:53:27.958369", + "version": 0, + "url": "https://ge.xhamster.com/videos/lezione-di-sesso-1980-italy-dominique-saint-claire-dvd-xh1Ldzu", + "review": 0, + "should_download": 0, + "title": "Lezione Di Sesso 1980 Italy Dominique Saint Claire Dvd | xHamster", + "file_name": "Lezione di sesso (1980, Italy, Dominique Saint Claire, DVD) [xh1Ldzu].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/b9284d00-2673-4d03-9a16-923f14d30394.mp4" + }, + { + "id": "b9364d25-bcd5-401c-9743-cbb1d4b34e90", + "created_date": "2024-07-25 07:29:46.964078", + "last_modified_date": "2024-07-25 07:29:46.964078", + "version": 0, + "url": "https://ge.xhamster.com/videos/karina-grand-dp-3855463", + "review": 0, + "should_download": 0, + "title": "Karina Grand DP: Free Threesome Porn Video ee | xHamster", + "file_name": "Karina Grand DP [3855463].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b9364d25-bcd5-401c-9743-cbb1d4b34e90.mp4" + }, + { + "id": "b938bdba-02a5-40ff-8677-6a8c370bc213", + "created_date": "2024-07-25 07:29:45.237614", + "last_modified_date": "2024-07-25 07:29:45.237614", + "version": 0, + "url": "https://ge.xhamster.com/videos/bisex-stepmom-sometimes-needs-cock-13735412", + "review": 0, + "should_download": 0, + "title": "Bisex-Stiefmutter braucht manchmal Schwanz | xHamster", + "file_name": "Bisex-Stiefmutter braucht manchmal Schwanz [13735412].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b938bdba-02a5-40ff-8677-6a8c370bc213.mp4" + }, + { + "id": "b97a109a-17a2-4fff-a6a8-f5b15cf78382", + "created_date": "2024-07-25 07:29:46.289893", + "last_modified_date": "2024-07-25 07:29:46.289893", + "version": 0, + "url": "https://ge.xhamster.com/videos/orgy-at-the-river-14429799", + "review": 0, + "should_download": 0, + "title": "Orgy at the river | xHamster", + "file_name": "Orgy at the river [14429799].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b97a109a-17a2-4fff-a6a8-f5b15cf78382.mp4" + }, + { + "id": "b9ac8b59-4bb3-4633-afc6-5e64bcdbcfb4", + "created_date": "2024-07-25 07:29:46.652308", + "last_modified_date": "2024-07-25 07:29:46.652308", + "version": 0, + "url": "https://ge.xhamster.com/videos/horny-stepdaughter-coco-lovelock-tries-stepdads-cock-in-rough-threesome-with-milf-kenzie-taylor-xhbFshz", + "review": 0, + "should_download": 0, + "title": "Die geile stieftochter Coco LoveLock versucht den schwanz des stiefvaters in grobem dreier mit MILF Kenzie Taylor | xHamster", + "file_name": "Die geile stieftochter Coco LoveLock versucht den schwanz des stiefvaters in grobem dreier mit MILF Kenzie Taylor [xhbFshz].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b9ac8b59-4bb3-4633-afc6-5e64bcdbcfb4.mp4" + }, + { + "id": "b9eecdd4-88c7-4d0e-8313-762a5aa2ea44", + "created_date": "2024-07-25 07:29:45.487885", + "last_modified_date": "2024-07-25 07:29:45.487885", + "version": 0, + "url": "https://ge.xhamster.com/videos/ein-sommer-im-zeichen-von-pussy-full-movie-xh4Jaxj", + "review": 0, + "should_download": 0, + "title": "Ein Sommer Im Zeichen Von Pussy Full Movie: Free Porn c8 | xHamster", + "file_name": "Ein Sommer im Zeichen von Pussy (Full Movie) [xh4Jaxj].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/b9eecdd4-88c7-4d0e-8313-762a5aa2ea44.mp4" + }, + { + "id": "b9fad5b8-456b-4d60-aa8d-dff9d90a48b2", + "created_date": "2024-07-25 07:29:44.879492", + "last_modified_date": "2024-07-25 07:29:44.879492", + "version": 0, + "url": "https://ge.xhamster.com/videos/loving-family-2-xhNdokk", + "review": 0, + "should_download": 0, + "title": "Liebende Familie 2 | xHamster", + "file_name": "Liebende Familie 2 [xhNdokk].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/b9fad5b8-456b-4d60-aa8d-dff9d90a48b2.mp4" + }, + { + "id": "ba081073-8c6e-4309-9041-23f34433c7f7", + "created_date": "2024-07-25 07:29:47.986349", + "last_modified_date": "2024-07-25 07:29:47.986349", + "version": 0, + "url": "https://ge.xhamster.com/videos/lust-weekend-1988-us-sharon-mitchell-full-video-dvdrip-xhTkKEE", + "review": 0, + "should_download": 0, + "title": "Lust-Wochenende (1988, wir, Sharon Mitchell, komplettes Video, dvdrip) | xHamster", + "file_name": "Lust-Wochenende (1988, wir, Sharon Mitchell, komplettes Video, dvdrip) [xhTkKEE].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ba081073-8c6e-4309-9041-23f34433c7f7.mp4" + }, + { + "id": "ba45cb1e-7ecc-41ce-909b-043520bf961e", + "created_date": "2024-10-21 15:08:43.543606", + "last_modified_date": "2024-10-21 16:31:44.107000", + "version": 1, + "url": "https://ge.xhamster.com/videos/secret-ritual-at-the-office-7634370", + "review": 0, + "should_download": 0, + "title": "Geheimes Ritual im B\u00fcro | xHamster", + "file_name": "Geheimes Ritual im B\u00fcro [7634370].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/ba45cb1e-7ecc-41ce-909b-043520bf961e.mp4" + }, + { + "id": "ba5ae46f-577c-46fa-9f9f-bac77dc8d528", + "created_date": "2024-07-25 07:29:45.862018", + "last_modified_date": "2024-07-25 07:29:45.862018", + "version": 0, + "url": "https://ge.xhamster.com/videos/fick-academy-aka-college-a-tout-faire-1977-9452790", + "review": 0, + "should_download": 0, + "title": "Fick Academy, College auch bekannt als A tout faire (1977) | xHamster", + "file_name": "Fick Academy, College auch bekannt als A tout faire (1977) [9452790].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ba5ae46f-577c-46fa-9f9f-bac77dc8d528.mp4" + }, + { + "id": "bb46f4fa-a0ae-46ca-963f-4effd2e16e04", + "created_date": "2024-07-25 07:29:47.337305", + "last_modified_date": "2024-07-25 07:29:47.337305", + "version": 0, + "url": "https://ge.xhamster.com/videos/eroticax-teen-tries-out-swinger-lifestyle-xhjtwpg", + "review": 0, + "should_download": 0, + "title": "Eroticax - ein Teenager probiert den Swinger-Lifestyle aus | xHamster", + "file_name": "Eroticax - ein Teenager probiert den Swinger-Lifestyle aus [xhjtwpg].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/bb46f4fa-a0ae-46ca-963f-4effd2e16e04.mp4" + }, + { + "id": "bb472ff0-a885-44e0-bdca-d340364400a6", + "created_date": "2024-07-25 07:29:46.260157", + "last_modified_date": "2024-07-25 07:29:46.260157", + "version": 0, + "url": "https://ge.xhamster.com/videos/stp7-a-family-that-love-their-weekly-get-together-8246579", + "review": 0, + "should_download": 0, + "title": "Stp7 eine Familie, die ihr w\u00f6chentliches Treffen liebt! | xHamster", + "file_name": "Stp7 eine Familie, die ihr w\u00f6chentliches Treffen liebt! [8246579].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/bb472ff0-a885-44e0-bdca-d340364400a6.mp4" + }, + { + "id": "bb583b0c-2c51-4d42-b10f-bee5998ea257", + "created_date": "2024-07-25 07:29:48.044685", + "last_modified_date": "2024-07-25 07:29:48.044685", + "version": 0, + "url": "https://ge.xhamster.com/videos/kelly-the-coed-9-xhLJndr", + "review": 0, + "should_download": 0, + "title": "Kelly the Coed # 9 | xHamster", + "file_name": "Kelly the Coed # 9 [xhLJndr].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/bb583b0c-2c51-4d42-b10f-bee5998ea257.mp4" + }, + { + "id": "bbf86b63-0d9f-44dd-afa4-af6a16313368", + "created_date": "2024-07-25 07:29:46.610826", + "last_modified_date": "2024-07-25 07:29:46.610826", + "version": 0, + "url": "https://ge.xhamster.com/videos/gruppensex-total-4-1992-xhR4K42", + "review": 0, + "should_download": 0, + "title": "Gruppensex Total 4 1992, Free In English Porn 9d | xHamster", + "file_name": "Gruppensex Total 4 (1992) [xhR4K42].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/bbf86b63-0d9f-44dd-afa4-af6a16313368.mp4" + }, + { + "id": "bca9c388-353c-463d-a50c-f3ca469ca2f1", + "created_date": "2024-07-25 07:29:47.926842", + "last_modified_date": "2024-07-25 07:29:47.926842", + "version": 0, + "url": "https://ge.xhamster.com/videos/tushy-shy-avery-seduces-has-anal-with-her-longtime-crush-xhfaV01", + "review": 0, + "should_download": 0, + "title": "Tushy, sch\u00fcchtern, Avery verf\u00fchrt & hat Anal mit ihrem langj\u00e4hrigen Schwarm | xHamster", + "file_name": "Tushy, sch\u00fcchtern, Avery verf\u00fchrt & hat Anal mit ihrem langj\u00e4hrigen Schwarm [xhfaV01].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/bca9c388-353c-463d-a50c-f3ca469ca2f1.mp4" + }, + { + "id": "bce19187-cf3b-472c-8abd-9e2fe6964977", + "created_date": "2024-07-25 07:29:47.624325", + "last_modified_date": "2024-07-25 07:29:47.624325", + "version": 0, + "url": "https://ge.xhamster.com/videos/das-dorf-der-feuchten-fotzen-1975-with-vivian-smith-6014330", + "review": 0, + "should_download": 0, + "title": "Das Dorf Der Feuchten Fotzen 1975 with Vivian Smith | xHamster", + "file_name": "Das Dorf der feuchten Fotzen (1975) with Vivian Smith [6014330].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/bce19187-cf3b-472c-8abd-9e2fe6964977.mp4" + }, + { + "id": "bd00ade2-4198-4de5-8256-9fc1cd188e9e", + "created_date": "2024-07-25 07:29:45.105513", + "last_modified_date": "2024-07-25 07:29:45.105513", + "version": 0, + "url": "https://ge.xhamster.com/videos/redhead-casted-british-cleaning-the-pipes-7409931", + "review": 0, + "should_download": 0, + "title": "Rothaarige bespritzt Briten beim Putzen der Rohre | xHamster", + "file_name": "Rothaarige bespritzt Briten beim Putzen der Rohre [7409931].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/bd00ade2-4198-4de5-8256-9fc1cd188e9e.mp4" + }, + { + "id": "bd25ae1e-4cd2-4c24-9fa3-438d548010e3", + "created_date": "2024-07-25 07:29:47.024744", + "last_modified_date": "2024-07-25 07:29:47.024744", + "version": 0, + "url": "https://ge.xhamster.com/videos/open-marriage-couple-pick-up-teen-for-wet-juices-threesome-xhHofg1", + "review": 0, + "should_download": 0, + "title": "Offene Ehepaare heben Teen f\u00fcr Dreier mit nassen S\u00e4ften auf | xHamster", + "file_name": "Offene Ehepaare heben Teen f\u00fcr Dreier mit nassen S\u00e4ften auf [xhHofg1].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/bd25ae1e-4cd2-4c24-9fa3-438d548010e3.mp4" + }, + { + "id": "bd358689-16c1-4d8d-b762-8e0e5f4846b6", + "created_date": "2024-10-07 20:47:56.430267", + "last_modified_date": "2024-10-21 16:31:48.213000", + "version": 1, + "url": "https://ge.xhamster.com/videos/nach-der-hochzeit-im-hallenbad-gefickt-2231403", + "review": 0, + "should_download": 0, + "title": "Nach Der Hochzeit Im Hallenbad Gefickt, Porn 2a | xHamster", + "file_name": "Nach der Hochzeit im Hallenbad gefickt [2231403].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/bd358689-16c1-4d8d-b762-8e0e5f4846b6.mp4" + }, + { + "id": "bd389f6a-5b0c-4707-a7dd-a12f5fb6e162", + "created_date": "2024-07-25 07:29:45.195869", + "last_modified_date": "2024-07-25 07:29:45.195869", + "version": 0, + "url": "https://ge.xhamster.com/videos/married-couple-talk-younger-neighbor-into-a-threesome-12233465", + "review": 0, + "should_download": 0, + "title": "Ehepaar \u00fcberredet j\u00fcngeren Nachbarn zu einem Dreier | xHamster", + "file_name": "Ehepaar \u00fcberredet j\u00fcngeren Nachbarn zu einem Dreier [12233465].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/bd389f6a-5b0c-4707-a7dd-a12f5fb6e162.mp4" + }, + { + "id": "bd48b9cf-c4e5-487f-8750-0ccfee4926b9", + "created_date": "2024-07-25 07:29:47.722093", + "last_modified_date": "2024-07-25 07:29:47.722093", + "version": 0, + "url": "https://ge.xhamster.com/videos/entangled-1993-xhjPDFN", + "review": 0, + "should_download": 0, + "title": "Entangled (1993) | xHamster", + "file_name": "Entangled (1993) [xhjPDFN].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/bd48b9cf-c4e5-487f-8750-0ccfee4926b9.mp4" + }, + { + "id": "bd903746-772b-4bd0-9f34-6d3ddb7c40b2", + "created_date": "2024-07-25 07:29:47.264956", + "last_modified_date": "2024-07-25 07:29:47.264956", + "version": 0, + "url": "https://ge.xhamster.com/videos/secretary-inside-1993-xh1VUlX", + "review": 0, + "should_download": 0, + "title": "Secretary Inside 1993, Free European Porn 06 | xHamster", + "file_name": "Secretary Inside (1993) [xh1VUlX].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/bd903746-772b-4bd0-9f34-6d3ddb7c40b2.mp4" + }, + { + "id": "bd922d44-9b45-48e5-b09e-bc9946e5b6a2", + "created_date": "2024-07-25 07:29:44.707030", + "last_modified_date": "2024-07-25 07:29:44.707030", + "version": 0, + "url": "https://ge.xhamster.com/videos/18-and-confused-2-xhrowbj", + "review": 0, + "should_download": 0, + "title": "18 und verwirrt # 2 | xHamster", + "file_name": "18 und verwirrt # 2 [xhrowbj].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/bd922d44-9b45-48e5-b09e-bc9946e5b6a2.mp4" + }, + { + "id": "be12b4f6-2af5-4c04-99aa-d35fc71bd0cd", + "created_date": "2024-07-25 07:29:46.306001", + "last_modified_date": "2024-07-25 07:29:46.306001", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-sex-exam-10511897", + "review": 0, + "should_download": 0, + "title": "Die Sex-Untersuchung | xHamster", + "file_name": "Die Sex-Untersuchung [10511897].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/be12b4f6-2af5-4c04-99aa-d35fc71bd0cd.mp4" + }, + { + "id": "be14b4ea-59a0-4fa7-9112-a2b65d75d455", + "created_date": "2024-08-28 23:21:54.366218", + "last_modified_date": "2024-08-28 23:21:54.366218", + "version": 0, + "url": "https://ge.xhamster.com/videos/forbidden-sex-games-1986-xhjfy3w", + "review": 0, + "should_download": 0, + "title": "Verbotene Sex-Spiele (1986) | xHamster", + "file_name": "Verbotene Sex-Spiele (1986) [xhjfy3w].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/be14b4ea-59a0-4fa7-9112-a2b65d75d455.mp4" + }, + { + "id": "be476378-45a3-4101-9894-2555bc5722c1", + "created_date": "2024-07-25 07:29:45.004740", + "last_modified_date": "2024-07-25 07:29:45.004740", + "version": 0, + "url": "https://ge.xhamster.com/videos/milf-katrina-colt-says-you-just-need-a-more-experienced-woman-to-help-you-s20-e2-xhkkZiG", + "review": 0, + "should_download": 0, + "title": "Milf Katrina Colt sagt, \"Sie brauchen nur eine erfahrenere Frau, um ihnen zu helfen\" - S20: E2 | xHamster", + "file_name": "Milf Katrina Colt sagt, \uff02Sie brauchen nur eine erfahrenere Frau, um ihnen zu helfen\uff02 - S20\uff1a E2 [xhkkZiG].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/be476378-45a3-4101-9894-2555bc5722c1.mp4" + }, + { + "id": "be4b3710-5bca-4cf8-bb85-568f78bfec20", + "created_date": "2024-07-25 07:29:46.293811", + "last_modified_date": "2024-07-25 07:29:46.293811", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsister-decided-to-have-sex-with-stepbrother-while-parents-are-not-at-home-syndicete-xhb88V7", + "review": 0, + "should_download": 0, + "title": "Stiefschwester beschloss, Sex mit Stiefbruder zu haben, w\u00e4hrend die Eltern nicht zu Hause sind - Syndicete | xHamster", + "file_name": "Stiefschwester beschloss, Sex mit Stiefbruder zu haben, w\u00e4hrend die Eltern nicht zu Hause sind - Syndicete [xhb88V7].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/be4b3710-5bca-4cf8-bb85-568f78bfec20.mp4" + }, + { + "id": "be52ce42-dd54-49f0-b599-c7136b54582d", + "created_date": "2024-07-25 07:29:45.021863", + "last_modified_date": "2024-07-25 07:29:45.021863", + "version": 0, + "url": "https://ge.xhamster.com/videos/bratty-sis-step-brother-and-sister-get-caught-fucking-s3-e-8770848", + "review": 0, + "should_download": 0, + "title": "Bratty sis - Stiefbruder und Schwester werden beim Ficken bei s3: e erwischt | xHamster", + "file_name": "Bratty sis - Stiefbruder und Schwester werden beim Ficken bei s3\uff1a e erwischt [8770848].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/be52ce42-dd54-49f0-b599-c7136b54582d.mp4" + }, + { + "id": "be91eba0-0f6f-4e6e-a9f6-13221f8f26e4", + "created_date": "2024-07-25 07:29:44.783695", + "last_modified_date": "2024-07-25 07:29:44.783695", + "version": 0, + "url": "https://ge.xhamster.com/videos/momsteachsex-mom-and-step-son-share-bed-and-fuck-s7-e3-8735140", + "review": 0, + "should_download": 0, + "title": "Momsteachsex - Mutter und Stiefsohn teilen sich das Bett und ficken s7: e3 | xHamster", + "file_name": "Momsteachsex - Mutter und Stiefsohn teilen sich das Bett und ficken s7\uff1a e3 [8735140].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/be91eba0-0f6f-4e6e-a9f6-13221f8f26e4.mp4" + }, + { + "id": "bf0d9c52-fb5f-4035-bee2-eee3d3661849", + "created_date": "2024-10-21 15:08:43.555609", + "last_modified_date": "2024-10-21 16:31:52.522000", + "version": 1, + "url": "https://ge.xhamster.com/videos/night-at-the-roxburys-10064260", + "review": 0, + "should_download": 0, + "title": "Nacht bei den Roxbury's | xHamster", + "file_name": "Nacht bei den Roxbury's [10064260].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/bf0d9c52-fb5f-4035-bee2-eee3d3661849.mp4" + }, + { + "id": "bfa37f78-a5ac-47b6-92b0-22c01984482a", + "created_date": "2024-07-25 07:29:45.796804", + "last_modified_date": "2024-07-25 07:29:45.796804", + "version": 0, + "url": "https://ge.xhamster.com/videos/american-college-xxx-vol-8-original-version-in-hd-xhaG41o", + "review": 0, + "should_download": 0, + "title": "American College xxx - vol (8) - (Originalversion in hd) | xHamster", + "file_name": "American College xxx - vol (8) - (Originalversion in hd) [xhaG41o].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/bfa37f78-a5ac-47b6-92b0-22c01984482a.mp4" + }, + { + "id": "c027e260-1312-43d5-baa2-09e75742d6d6", + "created_date": "2025-01-19 13:42:27.340286", + "last_modified_date": "2025-01-19 13:42:27.340293", + "version": 0, + "url": "https://ge.xhamster.com/videos/best-friends-you-give-a-kiss-and-the-cock-at-the-same-time-xhMbGyP", + "review": 0, + "should_download": 0, + "title": "besten freunden gibt man ein K\u00fcsschen und den Schwanz gleich dazu | xHamster", + "file_name": "besten freunden gibt man ein K\u00fcsschen und den Schwanz gleich dazu [xhMbGyP].mp4", + "path": null, + "cloud_link": null + }, + { + "id": "c117fa7d-e04d-4950-b85b-31337b30a17f", + "created_date": "2024-07-25 07:29:45.828603", + "last_modified_date": "2024-07-25 07:29:45.828603", + "version": 0, + "url": "https://ge.xhamster.com/videos/haley-spades-convinces-stepbro-we-should-role-play-together-s8-e4-xhl9dJP", + "review": 0, + "should_download": 0, + "title": "Haley Spades \u00fcberzeugt Stiefbruder: \"Wir sollten zusammen Rollenspiele machen!\" - s8: e4 | xHamster", + "file_name": "Haley Spades \u00fcberzeugt Stiefbruder\uff1a \uff02Wir sollten zusammen Rollenspiele machen!\uff02 - s8\uff1a e4 [xhl9dJP].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c117fa7d-e04d-4950-b85b-31337b30a17f.mp4" + }, + { + "id": "c12ba4e3-4dc0-481f-837f-04ad821a14cb", + "created_date": "2024-07-25 07:29:44.906642", + "last_modified_date": "2024-07-25 07:29:44.906642", + "version": 0, + "url": "https://ge.xhamster.com/videos/der-hausmeister-full-movie-xhpC5HPO", + "review": 0, + "should_download": 0, + "title": "Der Hausmeister Full Movie, Free German Porn d3 | xHamster", + "file_name": "Der Hausmeister (Full Movie) [xhpC5HPO].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c12ba4e3-4dc0-481f-837f-04ad821a14cb.mp4" + }, + { + "id": "c16be31d-84c3-424f-8be8-7794ce878e3d", + "created_date": "2024-07-25 07:29:45.312891", + "last_modified_date": "2024-07-25 07:29:45.312891", + "version": 0, + "url": "https://ge.xhamster.com/videos/2-cocks-and-sperm-instead-of-sauna-infusion-xhpxej2", + "review": 0, + "should_download": 0, + "title": "2 Schw\u00e4nze und Sperma statt Sauna Aufguss | xHamster", + "file_name": "2 Schw\u00e4nze und Sperma statt Sauna Aufguss [xhpxej2].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c16be31d-84c3-424f-8be8-7794ce878e3d.mp4" + }, + { + "id": "c1bb1c9c-d4b5-469b-85d3-2e2ebc5b73f4", + "created_date": "2024-07-25 07:29:44.265675", + "last_modified_date": "2024-07-25 07:29:44.265675", + "version": 0, + "url": "https://ge.xhamster.com/videos/working-girls-1983-12151003", + "review": 0, + "should_download": 0, + "title": "Working Girls (1983) | xHamster", + "file_name": "Working Girls (1983) [12151003].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c1bb1c9c-d4b5-469b-85d3-2e2ebc5b73f4.mp4" + }, + { + "id": "c2236975-eb51-4bb0-b594-386b52e9f7fe", + "created_date": "2024-07-25 07:29:44.327775", + "last_modified_date": "2024-07-25 07:29:44.327775", + "version": 0, + "url": "https://ge.xhamster.com/videos/adult-time-college-boys-gangbang-bukkake-dp-pawg-heather-honey-in-public-library-full-scene-xhyoNBR", + "review": 0, + "should_download": 0, + "title": "ERWACHSENENZEIT - college-jungs GANGBANG BUKKAKE + DOPPELPENETRATION PAWG heather honey in der \u00d6FFENTLICHEN BIBLIOTHEK! GANZE SZENE | xHamster", + "file_name": "ERWACHSENENZEIT - college-jungs GANGBANG BUKKAKE + DOPPELPENETRATION PAWG heather honey in der \u00d6FFENTLICHEN BIBLIOTHEK! GANZE SZENE [xhyoNBR].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c2236975-eb51-4bb0-b594-386b52e9f7fe.mp4" + }, + { + "id": "c2296707-ff37-45ee-9ee7-abef5e33c7c1", + "created_date": "2024-07-25 07:29:47.552118", + "last_modified_date": "2024-07-25 07:29:47.552118", + "version": 0, + "url": "https://ge.xhamster.com/videos/unnatural-family-8749233", + "review": 0, + "should_download": 0, + "title": "Unnat\u00fcrliche Familie | xHamster", + "file_name": "Unnat\u00fcrliche Familie [8749233].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c2296707-ff37-45ee-9ee7-abef5e33c7c1.mp4" + }, + { + "id": "c2592a5e-6ffb-4d65-91af-d87258913aa3", + "created_date": "2024-09-11 10:23:29.175035", + "last_modified_date": "2024-10-21 16:31:57.497000", + "version": 1, + "url": "https://ge.xhamster.com/videos/surprise-youre-fucking-your-friends-hot-mom-today-xhKZypp", + "review": 0, + "should_download": 0, + "title": "\u00dcberraschung! Du fickst heute die hei\u00dfe mutter deines freundes! | xHamster", + "file_name": "\u00dcberraschung! Du fickst heute die hei\u00dfe mutter deines freundes! [xhKZypp].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/c2592a5e-6ffb-4d65-91af-d87258913aa3.mp4" + }, + { + "id": "c2a90005-ee9b-42a2-a523-eab2b433686f", + "created_date": "2024-10-14 20:33:38.257731", + "last_modified_date": "2024-10-21 16:32:01.227000", + "version": 1, + "url": "https://ge.xhamster.com/videos/the-sex-play-7452305", + "review": 0, + "should_download": 0, + "title": "Das Sexspiel | xHamster", + "file_name": "Das Sexspiel [7452305].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/c2a90005-ee9b-42a2-a523-eab2b433686f.mp4" + }, + { + "id": "c2ae2c0a-b32d-4149-ab38-e632eb198903", + "created_date": "2024-07-25 07:29:46.107013", + "last_modified_date": "2024-07-25 07:29:46.107013", + "version": 0, + "url": "https://ge.xhamster.com/videos/step-mom-and-stepson-summer-vacation-together-shiny-cock-films-xh5TMez", + "review": 0, + "should_download": 0, + "title": "Step Mom and Stepson Summer Vacation Together - Shiny Cock Films | xHamster", + "file_name": "Step Mom and Stepson Summer Vacation Together - Shiny Cock Films [xh5TMez].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c2ae2c0a-b32d-4149-ab38-e632eb198903.mp4" + }, + { + "id": "c2e86097-04c1-4191-b142-90d9295823ee", + "created_date": "2024-07-25 07:29:45.948391", + "last_modified_date": "2024-07-25 07:29:45.948391", + "version": 0, + "url": "https://ge.xhamster.com/videos/moni-und-lisa-die-sextollen-schwestern-xh4bGe6", + "review": 0, + "should_download": 0, + "title": "Moni Und Lisa - Die Sextollen Schwestern, Porn fe | xHamster", + "file_name": "Moni und Lisa - Die sextollen Schwestern [xh4bGe6].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c2e86097-04c1-4191-b142-90d9295823ee.mp4" + }, + { + "id": "c3488d7a-1684-4c41-8f83-5afd95ad7761", + "created_date": "2024-07-25 07:29:48.098123", + "last_modified_date": "2024-07-25 07:29:48.098123", + "version": 0, + "url": "https://ge.xhamster.com/videos/familystrokes-stepsiblings-get-caught-fucking-by-stepmom-9711431", + "review": 0, + "should_download": 0, + "title": "In Familystrokes werden Stiefgeschwister von ihrer Stiefmutter beim Ficken erwischt | xHamster", + "file_name": "In Familystrokes werden Stiefgeschwister von ihrer Stiefmutter beim Ficken erwischt [9711431].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c3488d7a-1684-4c41-8f83-5afd95ad7761.mp4" + }, + { + "id": "c34bc24a-16e5-42c5-9e7f-2cbba6cfc086", + "created_date": "2024-07-25 07:29:48.010298", + "last_modified_date": "2024-07-25 07:29:48.010298", + "version": 0, + "url": "https://ge.xhamster.com/videos/teen-sucks-a-couple-of-dicks-on-the-couch-and-get-fucked-xhUrdLi", + "review": 0, + "should_download": 0, + "title": "Teen lutscht ein paar Schw\u00e4nze auf der Couch und wird gefickt | xHamster", + "file_name": "Teen lutscht ein paar Schw\u00e4nze auf der Couch und wird gefickt [xhUrdLi].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c34bc24a-16e5-42c5-9e7f-2cbba6cfc086.mp4" + }, + { + "id": "c36af489-de25-48ff-a6f5-8802278f1741", + "created_date": "2024-07-25 07:29:46.319286", + "last_modified_date": "2024-07-25 07:29:46.319286", + "version": 0, + "url": "https://ge.xhamster.com/videos/frau-wollen-full-movie-xhgl60a", + "review": 0, + "should_download": 0, + "title": "Frau Wollen Full Movie, Free Story Porn Video 4a | xHamster", + "file_name": "FRAU WOLLEN (Full Movie) [xhgl60a].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c36af489-de25-48ff-a6f5-8802278f1741.mp4" + }, + { + "id": "c39d7eb2-1b7c-4bbf-a6b6-968c8408f4b5", + "created_date": "2024-12-29 23:53:27.884164", + "last_modified_date": "2024-12-31 00:52:22.782000", + "version": 1, + "url": "https://ge.xhamster.com/videos/vixen-lana-rhoades-has-sex-with-her-boss-xhH7snP", + "review": 0, + "should_download": 0, + "title": "Vixen Lana Rhoades hat Sex mit ihrem Chef | xHamster", + "file_name": "Vixen Lana Rhoades hat Sex mit ihrem Chef [xhH7snP].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/c39d7eb2-1b7c-4bbf-a6b6-968c8408f4b5.mp4" + }, + { + "id": "c40c2ee8-7e51-47dc-bf0b-52cf68ece492", + "created_date": "2024-07-25 07:29:45.821435", + "last_modified_date": "2025-01-03 11:56:42.284000", + "version": 1, + "url": "https://ge.xhamster.com/videos/im-either-getting-dick-today-or-this-weekend-its-your-choice-veronica-church-tells-stepbro-s27-e2-xhxZPTF", + "review": 0, + "should_download": 0, + "title": "\"Ich habe entweder heute oder an diesem Wochenende schwanz, es ist deine Wahl\" Veronica Church sagt stiefbruer - S27: e2 | xHamster", + "file_name": "\uff02Ich habe entweder heute oder an diesem Wochenende schwanz, es ist deine Wahl\uff02 Veronica Church sagt stiefbruer - S27\uff1a e2 [xhxZPTF].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c40c2ee8-7e51-47dc-bf0b-52cf68ece492.mp4" + }, + { + "id": "c4207adb-6d25-4b1f-87c0-dd20b144899a", + "created_date": "2024-07-25 07:29:45.556477", + "last_modified_date": "2024-07-25 07:29:45.556477", + "version": 0, + "url": "https://ge.xhamster.com/videos/if-you-want-your-wife-not-to-fuck-your-brains-out-you-have-to-fuck-her-hard-and-rough-xhTWv0M", + "review": 0, + "should_download": 0, + "title": "Wenn du willst, dass deine ehefrau dein gehirn nicht ausfickt, musst du sie hart und hart ficken | xHamster", + "file_name": "Wenn du willst, dass deine ehefrau dein gehirn nicht ausfickt, musst du sie hart und hart ficken [xhTWv0M].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c4207adb-6d25-4b1f-87c0-dd20b144899a.mp4" + }, + { + "id": "c4608d49-1955-4d42-897f-c0fb1d3950b2", + "created_date": "2024-07-25 07:29:45.963784", + "last_modified_date": "2024-07-25 07:29:45.963784", + "version": 0, + "url": "https://ge.xhamster.com/videos/3-way-porn-wet-t-shirt-at-poolside-orgy-13388985", + "review": 0, + "should_download": 0, + "title": "3-Wege-Porno - nasses T-Shirt bei Orgie am Pool | xHamster", + "file_name": "3-Wege-Porno - nasses T-Shirt bei Orgie am Pool [13388985].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c4608d49-1955-4d42-897f-c0fb1d3950b2.mp4" + }, + { + "id": "c4644225-ed5f-4d54-b64e-fdb5a6500916", + "created_date": "2024-07-25 07:29:47.406721", + "last_modified_date": "2024-07-25 07:29:47.406721", + "version": 0, + "url": "https://ge.xhamster.com/videos/alisson-and-stepdaddy-pool-fun-xhNqDRG", + "review": 0, + "should_download": 0, + "title": "Alisson und Stiefvater haben Spa\u00df im Pool | xHamster", + "file_name": "Alisson und Stiefvater haben Spa\u00df im Pool [xhNqDRG].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c4644225-ed5f-4d54-b64e-fdb5a6500916.mp4" + }, + { + "id": "c4b23136-3b2b-4894-8814-546137d3abc0", + "created_date": "2024-07-25 07:29:44.711345", + "last_modified_date": "2024-07-25 07:29:44.711345", + "version": 0, + "url": "https://ge.xhamster.com/videos/having-fun-with-hot-italian-girl-in-a-nude-beach-xhB2LmC", + "review": 0, + "should_download": 0, + "title": "Spa\u00df mit hei\u00dfem italienischem M\u00e4dchen an einem FKK-Strand haben | xHamster", + "file_name": "Spa\u00df mit hei\u00dfem italienischem M\u00e4dchen an einem FKK-Strand haben [xhB2LmC].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/c4b23136-3b2b-4894-8814-546137d3abc0.mp4" + }, + { + "id": "c519e955-e0a9-4861-b529-6ea561ca5a25", + "created_date": "2024-07-25 07:29:46.496519", + "last_modified_date": "2024-07-25 07:29:46.496519", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-12900151", + "review": 0, + "should_download": 0, + "title": "Familie | xHamster", + "file_name": "Familie [12900151].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c519e955-e0a9-4861-b529-6ea561ca5a25.mp4" + }, + { + "id": "c5277049-5d36-4e4f-a397-363c9a88f48a", + "created_date": "2024-07-25 07:29:45.181667", + "last_modified_date": "2024-07-25 07:29:45.181667", + "version": 0, + "url": "https://ge.xhamster.com/videos/teufliches-spiel-full-movie-xhGKEHm", + "review": 0, + "should_download": 0, + "title": "Teufliches Spiel Full Movie, Free German Porn da | xHamster", + "file_name": "Teufliches Spiel (Full Movie) [xhGKEHm].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c5277049-5d36-4e4f-a397-363c9a88f48a.mp4" + }, + { + "id": "c52dca76-7cdb-4a75-a2cd-bd7c6770058c", + "created_date": "2024-12-29 23:53:27.908539", + "last_modified_date": "2024-12-29 23:53:27.908539", + "version": 0, + "url": "https://ge.xhamster.com/videos/just-vintage-333-xhafqYa", + "review": 0, + "should_download": 0, + "title": "Nur Retro 333 | xHamster", + "file_name": "Nur Retro 333 [xhafqYa].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/c52dca76-7cdb-4a75-a2cd-bd7c6770058c.mp4" + }, + { + "id": "c536305b-5fac-4034-8a69-a182ef7611d8", + "created_date": "2024-07-25 07:29:47.662245", + "last_modified_date": "2024-07-25 07:29:47.662245", + "version": 0, + "url": "https://ge.xhamster.com/videos/die-geilste-familie-569190", + "review": 0, + "should_download": 0, + "title": "Die Geilste Familie: Threesome Porn Video fd | xHamster", + "file_name": "Die Geilste Familie [569190].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c536305b-5fac-4034-8a69-a182ef7611d8.mp4" + }, + { + "id": "c5616acd-c67e-4b1b-bb75-79ad307bfeb3", + "created_date": "2024-07-25 07:29:45.892189", + "last_modified_date": "2024-07-25 07:29:45.892189", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-stone-clan-part-i-1991-german-good-dvd-rip-xh9qGSC", + "review": 0, + "should_download": 0, + "title": "The Stone Clan, Teil i (1991, deutsch, guter DVD-Rip | xHamster", + "file_name": "The Stone Clan, Teil i (1991, deutsch, guter DVD-Rip [xh9qGSC].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c5616acd-c67e-4b1b-bb75-79ad307bfeb3.mp4" + }, + { + "id": "c5dc4500-a1df-48ce-91ed-76a2883eae19", + "created_date": "2024-09-24 08:11:39.007595", + "last_modified_date": "2024-10-21 16:32:07.751000", + "version": 1, + "url": "https://ge.xhamster.com/videos/freeze-and-shut-up-threesome-taboo-role-play-8910110", + "review": 0, + "should_download": 0, + "title": "Frieren Sie ein und halten Sie die Klappe! - Dreier-Tabu-Rollenspiel | xHamster", + "file_name": "Frieren Sie ein und halten Sie die Klappe! - Dreier-Tabu-Rollenspiel [8910110].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/c5dc4500-a1df-48ce-91ed-76a2883eae19.mp4" + }, + { + "id": "c5fa99a9-2153-4a40-87b5-771287968c47", + "created_date": "2024-07-25 07:29:45.453292", + "last_modified_date": "2024-07-25 07:29:45.453292", + "version": 0, + "url": "https://ge.xhamster.com/videos/die-hammergeile-fick-gemeinschaft-1554728", + "review": 0, + "should_download": 0, + "title": "Die Hammergeile Fick-gemeinschaft, Free Porn 07 | xHamster", + "file_name": "Die Hammergeile Fick-Gemeinschaft [1554728].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c5fa99a9-2153-4a40-87b5-771287968c47.mp4" + }, + { + "id": "c60ecddc-02cb-4697-8ff3-286c44577897", + "created_date": "2024-07-25 07:29:46.960582", + "last_modified_date": "2024-07-25 07:29:46.960582", + "version": 0, + "url": "https://ge.xhamster.com/videos/tushy-assistant-gets-dpd-by-boss-and-friend-8876577", + "review": 0, + "should_download": 0, + "title": "Tushy Assistentin wird von Chef und Freund doppelpenetriert | xHamster", + "file_name": "Tushy Assistentin wird von Chef und Freund doppelpenetriert [8876577].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c60ecddc-02cb-4697-8ff3-286c44577897.mp4" + }, + { + "id": "c6179760-6609-40f2-be7a-afca62a63d3d", + "created_date": "2024-10-21 15:08:43.549535", + "last_modified_date": "2024-10-21 16:32:11.651000", + "version": 1, + "url": "https://ge.xhamster.com/videos/brigitte-lahaie-big-orgy-1979-7853468", + "review": 0, + "should_download": 0, + "title": "Brigitte Lahaie, Gro\u00dfe Orgie (1979) | xHamster", + "file_name": "Brigitte Lahaie, Gro\u00dfe Orgie (1979) [7853468].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/c6179760-6609-40f2-be7a-afca62a63d3d.mp4" + }, + { + "id": "c64eab78-18bb-430c-809b-6d5e11a61a37", + "created_date": "2024-07-25 07:29:46.915186", + "last_modified_date": "2024-07-25 07:29:46.915186", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsiblingscaught-horny-sister-needs-my-huge-cock-s7-e6-10044143", + "review": 0, + "should_download": 0, + "title": "Stepsiblingscaught - die geile Schwester braucht meinen riesigen Schwanz s7: e6 | xHamster", + "file_name": "Stepsiblingscaught - die geile Schwester braucht meinen riesigen Schwanz s7\uff1a e6 [10044143].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c64eab78-18bb-430c-809b-6d5e11a61a37.mp4" + }, + { + "id": "c65c1447-7fb0-4283-8a13-3bcfbbe57449", + "created_date": "2024-07-25 07:29:44.883074", + "last_modified_date": "2024-07-25 07:29:44.883074", + "version": 0, + "url": "https://ge.xhamster.com/videos/dirty-games-14463567", + "review": 0, + "should_download": 0, + "title": "Schmutzige Spiele | xHamster", + "file_name": "Schmutzige Spiele [14463567].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c65c1447-7fb0-4283-8a13-3bcfbbe57449.mp4" + }, + { + "id": "c6f4e8c8-23ca-4743-ac79-9efbed910800", + "created_date": "2024-12-29 23:53:27.926322", + "last_modified_date": "2024-12-29 23:53:27.926322", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-mischiefs-part-4-step-mom-makes-step-daughter-have-a-hot-threesome-with-her-bf-xhvARKo", + "review": 0, + "should_download": 0, + "title": "Familien-unfug (teil 4): Stiefmutter l\u00e4sst stieftochter einen hei\u00dfen dreier mit ihrem freund haben | xHamster", + "file_name": "Familien-unfug (teil 4)\uff1a Stiefmutter l\u00e4sst stieftochter einen hei\u00dfen dreier mit ihrem freund haben [xhvARKo].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/c6f4e8c8-23ca-4743-ac79-9efbed910800.mp4" + }, + { + "id": "c6ff2c83-ef4d-4d90-901b-0dd457004cc0", + "created_date": "2024-07-25 07:29:45.245181", + "last_modified_date": "2024-07-25 07:29:45.245181", + "version": 0, + "url": "https://ge.xhamster.com/videos/a1nyc-among-the-greatest-porn-films-ever-made-11109571", + "review": 0, + "should_download": 0, + "title": "A1nyc geh\u00f6rt zu den gr\u00f6\u00dften Pornofilmen, die je gemacht wurden | xHamster", + "file_name": "A1nyc geh\u00f6rt zu den gr\u00f6\u00dften Pornofilmen, die je gemacht wurden [11109571].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c6ff2c83-ef4d-4d90-901b-0dd457004cc0.mp4" + }, + { + "id": "c70c3f02-3918-4147-b1aa-80e09e9f7584", + "created_date": "2024-07-25 07:29:48.066784", + "last_modified_date": "2024-07-25 07:29:48.066784", + "version": 0, + "url": "https://ge.xhamster.com/videos/die-fickprobe-1991-xh9cN4w", + "review": 0, + "should_download": 0, + "title": "Die Fickprobe 1991: Free European Porn Video 76 | xHamster", + "file_name": "Die Fickprobe (1991) [xh9cN4w].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c70c3f02-3918-4147-b1aa-80e09e9f7584.mp4" + }, + { + "id": "c74499f2-87e3-48ab-8421-dd38e56a83e3", + "created_date": "2025-01-16 19:59:32.105320", + "last_modified_date": "2025-01-16 19:59:32.105326", + "version": 0, + "url": "https://ge.xhamster.com/videos/we-all-want-to-take-a-look-xhCoXz7", + "review": 0, + "should_download": 0, + "title": "Wir alle wollen einen Blick darauf werfen | xHamster", + "file_name": "Wir alle wollen einen Blick darauf werfen [xhCoXz7].mp4", + "path": null, + "cloud_link": "/data/media/c74499f2-87e3-48ab-8421-dd38e56a83e3.mp4" + }, + { + "id": "c77b91e9-af0c-4788-b634-c53de948f538", + "created_date": "2024-07-25 07:29:45.483304", + "last_modified_date": "2024-07-25 07:29:45.483304", + "version": 0, + "url": "https://ge.xhamster.com/videos/carnal-olympics-1983-xhtlcdR", + "review": 0, + "should_download": 0, + "title": "Carnal Olympics (1983) | xHamster", + "file_name": "Carnal Olympics (1983) [xhtlcdR].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c77b91e9-af0c-4788-b634-c53de948f538.mp4" + }, + { + "id": "c79817d3-1440-49a8-b5a1-9eed35ae4171", + "created_date": "2024-07-25 07:29:48.059587", + "last_modified_date": "2024-07-25 07:29:48.059587", + "version": 0, + "url": "https://ge.xhamster.com/videos/college-girls-1977-9357766", + "review": 0, + "should_download": 0, + "title": "College-M\u00e4dchen (1977) | xHamster", + "file_name": "College-M\u00e4dchen (1977) [9357766].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c79817d3-1440-49a8-b5a1-9eed35ae4171.mp4" + }, + { + "id": "c79a80ec-513d-4a4d-8eee-54e779262adc", + "created_date": "2024-08-09 19:34:02.478621", + "last_modified_date": "2024-08-16 10:31:50.801000", + "version": 1, + "url": "https://ge.xhamster.com/videos/party-ends-in-threesome-couple-fucks-their-friend-french-kissing-and-cum-licking-from-ass-mff-xhlfTiS", + "review": 0, + "should_download": 0, + "title": "Party endet mit dreier, paar fickt ihren freund, franz\u00f6sisches k\u00fcssen und sperma lecken vom arsch, MFF | xHamster", + "file_name": "Party endet mit dreier, paar fickt ihren freund, franz\u00f6sisches k\u00fcssen und sperma lecken vom arsch, MFF [xhlfTiS].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/c79a80ec-513d-4a4d-8eee-54e779262adc.mp4" + }, + { + "id": "c7cb2f55-d63d-4788-ba17-2d51f27c2c18", + "created_date": "2024-10-07 20:47:56.428842", + "last_modified_date": "2024-10-21 16:32:16.535000", + "version": 1, + "url": "https://ge.xhamster.com/videos/boss-fuck-my-wife-for-my-promotion-my-gift-for-you-xhHE33q", + "review": 0, + "should_download": 0, + "title": "Chef - Ficken Sie f\u00fcr meine Bef\u00f6rderung meine Frau! Mein Geschenk f\u00fcr Sie! | xHamster", + "file_name": "Chef - Ficken Sie f\u00fcr meine Bef\u00f6rderung meine Frau! Mein Geschenk f\u00fcr Sie! [xhHE33q].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/c7cb2f55-d63d-4788-ba17-2d51f27c2c18.mp4" + }, + { + "id": "c81ac77b-802c-4396-b105-d6165a4cfeb6", + "created_date": "2024-07-25 07:29:47.783198", + "last_modified_date": "2024-07-25 07:29:47.783198", + "version": 0, + "url": "https://ge.xhamster.com/videos/der-familienfick-10899040", + "review": 0, + "should_download": 0, + "title": "Der Familienfick: Free Porn Video 60 | xHamster", + "file_name": "Der Familienfick [10899040].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c81ac77b-802c-4396-b105-d6165a4cfeb6.mp4" + }, + { + "id": "c8443a9e-e0ff-4455-96ca-fe71e966ccfe", + "created_date": "2024-08-28 23:21:54.369515", + "last_modified_date": "2024-08-28 23:21:54.369515", + "version": 0, + "url": "https://ge.xhamster.com/videos/summer-camp-xhupN1m", + "review": 0, + "should_download": 0, + "title": "Sommerlager | xHamster", + "file_name": "Sommerlager [xhupN1m].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/c8443a9e-e0ff-4455-96ca-fe71e966ccfe.mp4" + }, + { + "id": "c8542277-e729-4b41-b9fa-cad44a398304", + "created_date": "2024-07-25 07:29:47.121406", + "last_modified_date": "2024-07-25 07:29:47.121406", + "version": 0, + "url": "https://ge.xhamster.com/videos/money-hunting-1994-italy-german-dub-full-dvdrip-xhSNEW3", + "review": 0, + "should_download": 0, + "title": "Money Hunting (1994, Italien, deutscher Dub, voll, dvdrip) | xHamster", + "file_name": "Money Hunting (1994, Italien, deutscher Dub, voll, dvdrip) [xhSNEW3].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c8542277-e729-4b41-b9fa-cad44a398304.mp4" + }, + { + "id": "c8dcb75a-d21a-41c7-8c73-23caab07ac70", + "created_date": "2024-07-25 07:29:46.956824", + "last_modified_date": "2024-07-25 07:29:46.956824", + "version": 0, + "url": "https://ge.xhamster.com/videos/jeannie-full-movie-xhZ5qGu", + "review": 0, + "should_download": 0, + "title": "Jeannie (kompletter Film) | xHamster", + "file_name": "Jeannie (kompletter Film) [xhZ5qGu].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c8dcb75a-d21a-41c7-8c73-23caab07ac70.mp4" + }, + { + "id": "c936e4fe-443a-4280-aa14-bb8a894f4bb7", + "created_date": "2024-07-25 07:29:45.116257", + "last_modified_date": "2024-07-25 07:29:45.116257", + "version": 0, + "url": "https://ge.xhamster.com/videos/step-mom-loves-anal-2-9345917", + "review": 0, + "should_download": 0, + "title": "Stiefmutter liebt Anal 2 | xHamster", + "file_name": "Stiefmutter liebt Anal 2 [9345917].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c936e4fe-443a-4280-aa14-bb8a894f4bb7.mp4" + }, + { + "id": "c93d3388-ade3-47fd-ab2a-d50be364c58e", + "created_date": "2025-01-16 19:59:42.081886", + "last_modified_date": "2025-01-16 19:59:42.081892", + "version": 0, + "url": "https://ge.xhamster.com/videos/young-sex-parties-bachelorette-party-with-a-strippers-3519983", + "review": 0, + "should_download": 0, + "title": "Junge Sexpartys - Junggesellinnenabschied mit Stripperinnen | xHamster", + "file_name": "Junge Sexpartys - Junggesellinnenabschied mit Stripperinnen [3519983].mp4", + "path": null, + "cloud_link": "/data/media/c93d3388-ade3-47fd-ab2a-d50be364c58e.mp4" + }, + { + "id": "c99883ce-711b-41a9-ad3b-79cdc1847d6d", + "created_date": "2024-09-24 08:11:39.001735", + "last_modified_date": "2024-10-21 16:32:21.546000", + "version": 1, + "url": "https://ge.xhamster.com/videos/found-wifes-friend-in-bed-with-us-in-the-morning-and-fucked-her-afterparty-fmf-xh662Ps", + "review": 0, + "should_download": 0, + "title": "Hab mit uns morgens mit uns die freundin der ehefrau im bett gefunden und sie gefickt, nach der party, Fmf | xHamster", + "file_name": "Hab mit uns morgens mit uns die freundin der ehefrau im bett gefunden und sie gefickt, nach der party, Fmf [xh662Ps].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/c99883ce-711b-41a9-ad3b-79cdc1847d6d.mp4" + }, + { + "id": "c9dbef20-30ef-48c3-9c18-fd5d13aa64f0", + "created_date": "2024-07-25 07:29:45.399767", + "last_modified_date": "2024-07-25 07:29:45.399767", + "version": 0, + "url": "https://ge.xhamster.com/videos/shes-my-best-friend-of-course-i-told-her-about-your-dick-tiffany-tatum-tells-stepbro-s4-e7-xhi7epR", + "review": 0, + "should_download": 0, + "title": "\"Sie ist meine beste freundin! Nat\u00fcrlich habe ich ihr von deinem schwanz erz\u00e4hlt\", sagt tiffany tatum, stiefbruer - s4: e7 | xHamster", + "file_name": "\uff02Sie ist meine beste freundin! Nat\u00fcrlich habe ich ihr von deinem schwanz erz\u00e4hlt\uff02, sagt tiffany tatum, stiefbruer - s4\uff1a e7 [xhi7epR].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c9dbef20-30ef-48c3-9c18-fd5d13aa64f0.mp4" + }, + { + "id": "c9ea31dd-5e5f-415d-a014-1b444e3994d2", + "created_date": "2024-07-25 07:29:44.289846", + "last_modified_date": "2024-07-25 07:29:44.289846", + "version": 0, + "url": "https://ge.xhamster.com/videos/our-girl-friend-caught-us-and-masturbated-looked-on-us-4k-xhTtd47", + "review": 0, + "should_download": 0, + "title": "Unsere Freundin hat uns erwischt und masturbiert und beobachtet uns 4k | xHamster", + "file_name": "Unsere Freundin hat uns erwischt und masturbiert und beobachtet uns 4k [xhTtd47].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/c9ea31dd-5e5f-415d-a014-1b444e3994d2.mp4" + }, + { + "id": "ca5126ef-cf68-4b39-abef-91487f03abef", + "created_date": "2024-08-28 23:21:54.379869", + "last_modified_date": "2024-08-28 23:21:54.379869", + "version": 0, + "url": "https://ge.xhamster.com/videos/nubile-films-best-friends-big-tit-teen-gf-sucks-and-fucks-10941965", + "review": 0, + "should_download": 0, + "title": "Nubile filmt - die beste Freundin des Teenagers mit dicken Titten lutscht und fickt | xHamster", + "file_name": "Nubile filmt - die beste Freundin des Teenagers mit dicken Titten lutscht und fickt [10941965].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/ca5126ef-cf68-4b39-abef-91487f03abef.mp4" + }, + { + "id": "ca719db8-6b3d-41d7-b231-5543f662a197", + "created_date": "2024-09-24 08:11:39.009989", + "last_modified_date": "2024-10-21 16:32:26.280000", + "version": 1, + "url": "https://ge.xhamster.com/videos/private-secretarial-services-1980-7346662", + "review": 0, + "should_download": 0, + "title": "Private Sekret\u00e4rinnendienste - 1980 | xHamster", + "file_name": "Private Sekret\u00e4rinnendienste - 1980 [7346662].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/ca719db8-6b3d-41d7-b231-5543f662a197.mp4" + }, + { + "id": "ca7e19d5-ec77-4648-bb58-d5add967b326", + "created_date": "2024-10-21 15:08:43.550571", + "last_modified_date": "2024-10-21 16:32:29.043000", + "version": 1, + "url": "https://ge.xhamster.com/videos/rita-cardinale-gangbang-and-bukkake-in-the-restaurant-6631530", + "review": 0, + "should_download": 0, + "title": "Rita Cardinale, Gangbang und Bukkake im Restaurant | xHamster", + "file_name": "Rita Cardinale, Gangbang und Bukkake im Restaurant [6631530].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/ca7e19d5-ec77-4648-bb58-d5add967b326.mp4" + }, + { + "id": "ca800ea6-d69a-4ab7-810a-71c50c1bed52", + "created_date": "2024-07-25 07:29:45.977970", + "last_modified_date": "2024-07-25 07:29:45.977970", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepson-and-friend-dp-stepmom-xhGcSVJ", + "review": 0, + "should_download": 0, + "title": "Stiefsohn und Freund doppelpenetrierte Stiefmutter | xHamster", + "file_name": "Stiefsohn und Freund doppelpenetrierte Stiefmutter [xhGcSVJ].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ca800ea6-d69a-4ab7-810a-71c50c1bed52.mp4" + }, + { + "id": "caa4e074-ec16-4ecd-9113-d6bc33f8944f", + "created_date": "2024-11-01 21:08:26.931698", + "last_modified_date": "2024-11-01 21:08:26.931698", + "version": 0, + "url": "https://ge.xhamster.com/videos/classic-1984-caught-from-behind-2-02-xhVXwpE", + "review": 0, + "should_download": 0, + "title": "Klassiker - 1984 - von hinten erwischt 2-02 | xHamster", + "file_name": "Klassiker - 1984 - von hinten erwischt 2-02 [xhVXwpE].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/caa4e074-ec16-4ecd-9113-d6bc33f8944f.mp4" + }, + { + "id": "cab032ae-965c-47a7-866f-1f03c04d9660", + "created_date": "2024-12-29 23:53:27.934520", + "last_modified_date": "2024-12-29 23:53:27.934520", + "version": 0, + "url": "https://ge.xhamster.com/videos/18th-birthday-presents-5864297", + "review": 0, + "should_download": 0, + "title": "18. Geburtstagsgeschenke | xHamster", + "file_name": "18. Geburtstagsgeschenke [5864297].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/cab032ae-965c-47a7-866f-1f03c04d9660.mp4" + }, + { + "id": "cb01b44f-5ed7-418e-887f-22314886c1ef", + "created_date": "2024-07-25 07:29:47.570810", + "last_modified_date": "2025-01-03 11:56:45.657000", + "version": 1, + "url": "https://ge.xhamster.com/videos/palmen-mee-und-nasse-grotten-1979-10328271", + "review": 0, + "should_download": 0, + "title": "Palmen Mee Und Nasse Grotten 1979, Free Porn 1a | xHamster", + "file_name": "Palmen Mee und nasse Grotten (1979) [10328271].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/cb01b44f-5ed7-418e-887f-22314886c1ef.mp4" + }, + { + "id": "cb31e9a8-2496-4062-890f-acca494be1b4", + "created_date": "2024-08-09 21:24:11.520580", + "last_modified_date": "2024-08-16 10:32:05.010000", + "version": 1, + "url": "https://ge.xhamster.com/videos/hot-on-a-stick-1989-xhvvHln", + "review": 0, + "should_download": 0, + "title": "Heiss am Stiel (1989) | xHamster", + "file_name": "Heiss am Stiel (1989) [xhvvHln].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/cb31e9a8-2496-4062-890f-acca494be1b4.mp4" + }, + { + "id": "cb3711f8-7fb7-48e1-8d5c-9c9dd40df0aa", + "created_date": "2024-07-25 07:29:45.974462", + "last_modified_date": "2024-07-25 07:29:45.974462", + "version": 0, + "url": "https://ge.xhamster.com/videos/tushy-babysitter-aspen-ora-fucked-in-the-ass-5481078", + "review": 0, + "should_download": 0, + "title": "Tushy Babysitter Aspen Ora Fucked in the Ass: Free Porn 80 | xHamster", + "file_name": "TUSHY Babysitter Aspen Ora Fucked In The Ass [5481078].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/cb3711f8-7fb7-48e1-8d5c-9c9dd40df0aa.mp4" + }, + { + "id": "cb48a881-8f2d-4fe9-bf20-84958dfc6cb2", + "created_date": "2024-11-01 21:08:26.938165", + "last_modified_date": "2024-11-01 21:08:26.938165", + "version": 0, + "url": "https://ge.xhamster.com/videos/getting-it-from-the-sister-in-law-stripper-xhlJn2d", + "review": 0, + "should_download": 0, + "title": "Es von der stripperin bekommen | xHamster", + "file_name": "Es von der stripperin bekommen [xhlJn2d].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/cb48a881-8f2d-4fe9-bf20-84958dfc6cb2.mp4" + }, + { + "id": "cb48bb60-cac7-4f1d-84ce-90bc0b8716a9", + "created_date": "2024-07-25 07:29:48.125741", + "last_modified_date": "2024-07-25 07:29:48.125741", + "version": 0, + "url": "https://ge.xhamster.com/videos/hardcore-sex-with-stepbrothers-cock-saves-slutty-anna-clair-clouds-distance-relationships-xh5JIId", + "review": 0, + "should_download": 0, + "title": "Hardcore-sex mit dem schwanz des stiefbruers rettet die distanzbeziehungen der versauten Anna Clair Cloud | xHamster", + "file_name": "Hardcore-sex mit dem schwanz des stiefbruers rettet die distanzbeziehungen der versauten Anna Clair Cloud [xh5JIId].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/cb48bb60-cac7-4f1d-84ce-90bc0b8716a9.mp4" + }, + { + "id": "cb6dbae3-4e4c-40ae-846f-42fcd1fb4b24", + "created_date": "2024-07-25 07:29:45.680667", + "last_modified_date": "2024-07-25 07:29:45.680667", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsis-alexa-grace-says-are-you-jerking-off-xhVf6bJ", + "review": 0, + "should_download": 0, + "title": "Stiefschwester Alexa Grace sagt: Wichst du ?! | xHamster", + "file_name": "Stiefschwester Alexa Grace sagt\uff1a Wichst du \uff1f! [xhVf6bJ].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/cb6dbae3-4e4c-40ae-846f-42fcd1fb4b24.mp4" + }, + { + "id": "cb76bb06-afec-4a2d-bdb5-1d6e6cb15a06", + "created_date": "2024-07-25 07:29:44.695820", + "last_modified_date": "2024-07-25 07:29:44.695820", + "version": 0, + "url": "https://ge.xhamster.com/videos/tushy-a-dp-with-my-husband-and-ex-boyfriend-8738285", + "review": 0, + "should_download": 0, + "title": "Tushy, ein Doppelpenetration mit meinem Ehemann und Ex-Freund | xHamster", + "file_name": "Tushy, ein Doppelpenetration mit meinem Ehemann und Ex-Freund [8738285].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/cb76bb06-afec-4a2d-bdb5-1d6e6cb15a06.mp4" + }, + { + "id": "cb96cc1f-f226-4026-a83f-377aea1a3314", + "created_date": "2024-07-25 07:29:45.352004", + "last_modified_date": "2024-07-25 07:29:45.352004", + "version": 0, + "url": "https://ge.xhamster.com/videos/schwester-xhUua2Q", + "review": 0, + "should_download": 0, + "title": "Schwester: Free Hardcore Porn Video ed | xHamster", + "file_name": "Schwester [xhUua2Q].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/cb96cc1f-f226-4026-a83f-377aea1a3314.mp4" + }, + { + "id": "cbc1c104-d114-40e7-8e68-34b27c668ff2", + "created_date": "2024-11-10 16:53:33.473895", + "last_modified_date": "2024-11-10 16:53:33.473895", + "version": 0, + "url": "https://ge.xhamster.com/videos/american-taboo-2-xhhjqYQ", + "review": 0, + "should_download": 0, + "title": "Amerikanisches Tabu 2 | xHamster", + "file_name": "Amerikanisches Tabu 2 [xhhjqYQ].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/cbc1c104-d114-40e7-8e68-34b27c668ff2.mp4" + }, + { + "id": "cce5bd94-7002-4d5f-b7c3-6d5fd92eacde", + "created_date": "2024-12-29 23:53:27.953282", + "last_modified_date": "2024-12-29 23:53:27.953282", + "version": 0, + "url": "https://ge.xhamster.com/videos/step-niece-has-always-been-careful-about-introducing-her-girlfriend-to-her-horny-step-uncle-xhwkAAs", + "review": 0, + "should_download": 0, + "title": "Stiefnichte ist sich immer vorsichtig damit, ihre freundin ihrem geilen stiefon vorgestellt zu haben | xHamster", + "file_name": "Stiefnichte ist sich immer vorsichtig damit, ihre freundin ihrem geilen stiefon vorgestellt zu haben [xhwkAAs].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/cce5bd94-7002-4d5f-b7c3-6d5fd92eacde.mp4" + }, + { + "id": "cd128f71-58a8-4c5e-9600-13c4fcc85456", + "created_date": "2024-10-07 20:47:56.409982", + "last_modified_date": "2024-10-21 16:32:33.427000", + "version": 1, + "url": "https://ge.xhamster.com/videos/double-up-in-daily-work-3877682", + "review": 0, + "should_download": 0, + "title": "Verdoppeln Sie in der t\u00e4glichen Arbeit! | xHamster", + "file_name": "Verdoppeln Sie in der t\u00e4glichen Arbeit! [3877682].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/cd128f71-58a8-4c5e-9600-13c4fcc85456.mp4" + }, + { + "id": "cd288c67-3cf8-4ec5-b87e-0baefe972051", + "created_date": "2024-07-25 07:29:46.929795", + "last_modified_date": "2024-07-25 07:29:46.929795", + "version": 0, + "url": "https://ge.xhamster.com/videos/buddys-secrets-1995-full-movie-xh40CDL", + "review": 0, + "should_download": 0, + "title": "Buddys Geheimnisse (1995) - kompletter Film | xHamster", + "file_name": "Buddys Geheimnisse (1995) - kompletter Film [xh40CDL].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/cd288c67-3cf8-4ec5-b87e-0baefe972051.mp4" + }, + { + "id": "cd449d35-ea0a-4a63-8234-7186f201be82", + "created_date": "2024-07-25 07:29:44.961717", + "last_modified_date": "2024-07-25 07:29:44.961717", + "version": 0, + "url": "https://ge.xhamster.com/videos/mamma-and-daddy-fuck-my-allies-7100646", + "review": 0, + "should_download": 0, + "title": "Mama und Papi ficken meine Verb\u00fcndeten | xHamster", + "file_name": "Mama und Papi ficken meine Verb\u00fcndeten [7100646].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/cd449d35-ea0a-4a63-8234-7186f201be82.mp4" + }, + { + "id": "cd4d35f8-6b74-4802-ab58-c12c7c0f611e", + "created_date": "2024-08-28 23:21:54.377070", + "last_modified_date": "2024-08-28 23:21:54.377070", + "version": 0, + "url": "https://ge.xhamster.com/videos/creepy-older-roommate-turns-on-attention-loving-sexy-thick-redhead-xhZvLzg", + "review": 0, + "should_download": 0, + "title": "Gruseliger \u00e4lterer mitbewohner macht aufmerksamkeit an, liebende sexy dicke rothaarige | xHamster", + "file_name": "Gruseliger \u00e4lterer mitbewohner macht aufmerksamkeit an, liebende sexy dicke rothaarige [xhZvLzg].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/cd4d35f8-6b74-4802-ab58-c12c7c0f611e.mp4" + }, + { + "id": "cdae8c01-14c9-403f-9a23-6ee5468c57fb", + "created_date": "2024-07-25 07:29:44.529507", + "last_modified_date": "2024-07-25 07:29:44.529507", + "version": 0, + "url": "https://ge.xhamster.com/videos/watching-boyfriend-fuck-my-college-best-friend-and-joining-13321068", + "review": 0, + "should_download": 0, + "title": "Zuschauen, wie mein Freund meine beste College-Freundin fickt und mitmachen | xHamster", + "file_name": "Zuschauen, wie mein Freund meine beste College-Freundin fickt und mitmachen [13321068].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/cdae8c01-14c9-403f-9a23-6ee5468c57fb.mp4" + }, + { + "id": "cdf514b1-2f91-4039-82a4-39e8db212a22", + "created_date": "2024-07-25 07:29:46.684217", + "last_modified_date": "2024-07-25 07:29:46.684217", + "version": 0, + "url": "https://ge.xhamster.com/videos/polizei-akademie-full-movie-hd-xhTHgRU", + "review": 0, + "should_download": 0, + "title": "Polizei Akademie Full Movie HD, Free Story HD Porn 2a | xHamster", + "file_name": "Polizei Akademie (Full Movie HD) [xhTHgRU].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/cdf514b1-2f91-4039-82a4-39e8db212a22.mp4" + }, + { + "id": "cebf2bd7-363a-48fe-9fc8-3bbe5907f683", + "created_date": "2024-07-25 07:29:47.786757", + "last_modified_date": "2024-07-25 07:29:47.786757", + "version": 0, + "url": "https://ge.xhamster.com/videos/mom-05-xhnUuKo", + "review": 0, + "should_download": 0, + "title": "Mutter 05 | xHamster", + "file_name": "Mutter 05 [xhnUuKo].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/cebf2bd7-363a-48fe-9fc8-3bbe5907f683.mp4" + }, + { + "id": "cee7b44b-8768-4e67-a25e-2f0587e474de", + "created_date": "2024-12-29 23:53:27.891471", + "last_modified_date": "2024-12-29 23:53:27.891471", + "version": 0, + "url": "https://ge.xhamster.com/videos/ondees-brulantes-1978-brigitte-lahaie-french-vintage-6986484", + "review": 0, + "should_download": 0, + "title": "Ondees Brulantes (1978) - Brigitte Lahaie - franz\u00f6sischer Retro | xHamster", + "file_name": "Ondees Brulantes (1978) - Brigitte Lahaie - franz\u00f6sischer Retro [6986484].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/cee7b44b-8768-4e67-a25e-2f0587e474de.mp4" + }, + { + "id": "cf1f54d3-131c-4be9-8da1-7d836b56f163", + "created_date": "2024-11-10 16:53:33.492237", + "last_modified_date": "2024-11-10 16:53:33.492237", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-whole-family-fucks-at-the-swingers-party-xhzWSEB", + "review": 0, + "should_download": 0, + "title": "Die ganze familie fickt auf der Swingerparty | xHamster", + "file_name": "Die ganze familie fickt auf der Swingerparty [xhzWSEB].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/cf1f54d3-131c-4be9-8da1-7d836b56f163.mp4" + }, + { + "id": "cf758d88-0b92-4dd4-8dd8-4ca611f0cff2", + "created_date": "2024-07-25 07:29:45.770613", + "last_modified_date": "2024-07-25 07:29:45.770613", + "version": 0, + "url": "https://ge.xhamster.com/videos/first-time-exhibition-xhGRpbG", + "review": 0, + "should_download": 0, + "title": "Zum ersten mal ausstellung | xHamster", + "file_name": "Zum ersten mal ausstellung [xhGRpbG].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/cf758d88-0b92-4dd4-8dd8-4ca611f0cff2.mp4" + }, + { + "id": "cf9fa6bd-e2e2-4250-8def-af2a70faaf4b", + "created_date": "2024-07-25 07:29:45.692438", + "last_modified_date": "2024-07-25 07:29:45.692438", + "version": 0, + "url": "https://ge.xhamster.com/videos/athena-anderson-says-i-love-watching-you-fuck-your-stepmom-s2-e3-xhokOrB", + "review": 0, + "should_download": 0, + "title": "Athena Anderson sagt: \"Ich liebe es, dich beim Ficken mit deiner Stiefmutter zu beobachten\" s2: e3 | xHamster", + "file_name": "Athena Anderson sagt\uff1a \uff02Ich liebe es, dich beim Ficken mit deiner Stiefmutter zu beobachten\uff02 s2\uff1a e3 [xhokOrB].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/cf9fa6bd-e2e2-4250-8def-af2a70faaf4b.mp4" + }, + { + "id": "cfaa9782-95c1-4551-b1d1-dc6d8c43f717", + "created_date": "2024-07-25 07:29:44.492215", + "last_modified_date": "2024-07-25 07:29:44.492215", + "version": 0, + "url": "https://ge.xhamster.com/videos/sabine-mallory-secretary-14136574", + "review": 0, + "should_download": 0, + "title": "Sabine Mallory - Sekret\u00e4rin | xHamster", + "file_name": "Sabine Mallory - Sekret\u00e4rin [14136574].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/cfaa9782-95c1-4551-b1d1-dc6d8c43f717.mp4" + }, + { + "id": "cfb65461-ef2d-4c5b-9fd4-aa3d6f12693c", + "created_date": "2024-07-25 07:29:45.043862", + "last_modified_date": "2024-07-25 07:29:45.043862", + "version": 0, + "url": "https://ge.xhamster.com/videos/slutty-stepmom-makes-a-deal-with-step-daughters-cute-tutor-to-date-and-fuck-her-virgin-girl-xhEpxEg", + "review": 0, + "should_download": 0, + "title": "Versaute stiefmutter macht einen deal mit dem s\u00fc\u00dfen lehrer der stieftochter und fickt ihr jungfr\u00e4uliches m\u00e4dchen | xHamster", + "file_name": "Versaute stiefmutter macht einen deal mit dem s\u00fc\u00dfen lehrer der stieftochter und fickt ihr jungfr\u00e4uliches m\u00e4dchen [xhEpxEg].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/cfb65461-ef2d-4c5b-9fd4-aa3d6f12693c.mp4" + }, + { + "id": "cfb9b3e7-73bc-4733-9622-a26c6e277d30", + "created_date": "2024-07-25 07:29:46.081091", + "last_modified_date": "2024-07-25 07:29:46.081091", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-kinky-family-8655519", + "review": 0, + "should_download": 0, + "title": "Die versaute Familie | xHamster", + "file_name": "Die versaute Familie [8655519].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/cfb9b3e7-73bc-4733-9622-a26c6e277d30.mp4" + }, + { + "id": "cfe14607-e107-47dc-9bf4-a3c6db42255a", + "created_date": "2024-07-25 07:29:48.110870", + "last_modified_date": "2024-07-25 07:29:48.110870", + "version": 0, + "url": "https://ge.xhamster.com/videos/freeuse-house-stepgrandpa-bangs-his-step-daughter-and-step-granddaughter-whenever-he-feels-like-it-xhcyjxM", + "review": 0, + "should_download": 0, + "title": "Freeuse House - Stepgrandpa Bangs His Step-daughter and Step-granddaughter Whenever He Feels Like it | xHamster", + "file_name": "FreeUse House - Stepgrandpa Bangs His Step-Daughter And Step-granddaughter Whenever He Feels Like It [xhcyjxM].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/cfe14607-e107-47dc-9bf4-a3c6db42255a.mp4" + }, + { + "id": "d0047e18-7a02-4229-bffb-418f95c8249e", + "created_date": "2024-07-25 07:29:44.914322", + "last_modified_date": "2024-07-25 07:29:44.914322", + "version": 0, + "url": "https://ge.xhamster.com/videos/great-exciting-threesome-on-the-boat-xhdWomb", + "review": 0, + "should_download": 0, + "title": "Toller aufregender Dreier auf dem Boot! | xHamster", + "file_name": "Toller aufregender Dreier auf dem Boot! [xhdWomb].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d0047e18-7a02-4229-bffb-418f95c8249e.mp4" + }, + { + "id": "d019d154-1aec-4fa9-9f0b-da29555a2918", + "created_date": "2024-07-25 07:29:47.514381", + "last_modified_date": "2024-07-25 07:29:47.514381", + "version": 0, + "url": "https://ge.xhamster.com/videos/teeny-strich-privat-10313120", + "review": 0, + "should_download": 0, + "title": "Teeny strich privat | xHamster", + "file_name": "Teeny strich privat [10313120].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d019d154-1aec-4fa9-9f0b-da29555a2918.mp4" + }, + { + "id": "d06a8498-c83f-43fe-acac-5155f1b88d15", + "created_date": "2024-07-25 07:29:45.408111", + "last_modified_date": "2024-07-25 07:29:45.408111", + "version": 0, + "url": "https://ge.xhamster.com/videos/mein-verfickter-urlaub-episode-5-xh2vztC", + "review": 0, + "should_download": 0, + "title": "Mein Verfickter Urlaub - Episode 5, Free Porn 73 | xHamster", + "file_name": "Mein verfickter Urlaub - Episode 5 [xh2vztC].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d06a8498-c83f-43fe-acac-5155f1b88d15.mp4" + }, + { + "id": "d0ccd522-6adb-4bd1-9c5f-fda0a57469b1", + "created_date": "2024-12-30 18:49:09.989000", + "last_modified_date": "2025-01-03 01:46:20.592000", + "version": 2, + "url": "https://ge.xhamster.com/videos/step-mom-caught-her-daughter-with-a-classmate-and-join-to-them-13978461", + "review": 0, + "should_download": 0, + "title": "Stiefmutter erwischt ihre Tochter mit einem Mitsch\u00fcler und schlie\u00dft sich ihnen an | xHamster", + "file_name": "Stiefmutter erwischt ihre Tochter mit einem Mitsch\u00fcler und schlie\u00dft sich ihnen an [13978461].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/d0ccd522-6adb-4bd1-9c5f-fda0a57469b1.mp4" + }, + { + "id": "d0d4f87c-e479-4823-ba30-a37a95ae9fd0", + "created_date": "2024-07-25 07:29:44.864929", + "last_modified_date": "2024-07-25 07:29:44.864929", + "version": 0, + "url": "https://ge.xhamster.com/videos/perfect-assed-wet-teens-win-a-swimming-competition-xh2A5G2", + "review": 0, + "should_download": 0, + "title": "Perfekte feuchte Teenager gewinnen einen Schwimmwettbewerb | xHamster", + "file_name": "Perfekte feuchte Teenager gewinnen einen Schwimmwettbewerb [xh2A5G2].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/d0d4f87c-e479-4823-ba30-a37a95ae9fd0.mp4" + }, + { + "id": "d0e9ba27-5cc6-4521-b22f-919c45ad0e8b", + "created_date": "2024-07-25 07:29:48.052248", + "last_modified_date": "2024-07-25 07:29:48.052248", + "version": 0, + "url": "https://ge.xhamster.com/videos/private-private-com-big-boobs-orgy-in-the-country-9737301", + "review": 0, + "should_download": 0, + "title": "Private.com Private.com Orgie mit gro\u00dfen M\u00f6psen auf dem Land | xHamster", + "file_name": "Private.com Private.com Orgie mit gro\u00dfen M\u00f6psen auf dem Land [9737301].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d0e9ba27-5cc6-4521-b22f-919c45ad0e8b.mp4" + }, + { + "id": "d11741e4-5e94-4595-b161-57bb7cf0df85", + "created_date": "2024-07-25 07:29:47.820510", + "last_modified_date": "2024-07-25 07:29:47.820510", + "version": 0, + "url": "https://ge.xhamster.com/videos/immer-muss-ich-mit-meinem-stiefbruder-schlafen-xhSzOSx", + "review": 0, + "should_download": 0, + "title": "Immer Muss Ich Mit Meinem Stiefbruder Schlafen: HD Porn b1 | xHamster", + "file_name": "Immer muss ich mit meinem Stiefbruder schlafen [xhSzOSx].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d11741e4-5e94-4595-b161-57bb7cf0df85.mp4" + }, + { + "id": "d13bc711-a755-4a68-817a-92b2ddd6bc58", + "created_date": "2024-11-01 21:08:26.936468", + "last_modified_date": "2024-11-01 21:08:26.936468", + "version": 0, + "url": "https://ge.xhamster.com/videos/die-geile-heidi-1990264", + "review": 0, + "should_download": 0, + "title": "Die Geile Heidi: Free Hardcore Porn Video ce | xHamster", + "file_name": "Die Geile Heidi [1990264].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/d13bc711-a755-4a68-817a-92b2ddd6bc58.mp4" + }, + { + "id": "d1aaf4bc-b307-45f9-94df-620642580388", + "created_date": "2024-07-25 07:29:47.766365", + "last_modified_date": "2024-07-25 07:29:47.766365", + "version": 0, + "url": "https://ge.xhamster.com/videos/college-wild-party-1424365", + "review": 0, + "should_download": 0, + "title": "College-wilde Party | xHamster", + "file_name": "College-wilde Party [1424365].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d1aaf4bc-b307-45f9-94df-620642580388.mp4" + }, + { + "id": "d1ff3e98-3148-43b0-bc48-3b332a0bb0ff", + "created_date": "2024-07-25 07:29:47.296158", + "last_modified_date": "2024-07-25 07:29:47.296158", + "version": 0, + "url": "https://ge.xhamster.com/videos/anal-nuru-massage-with-chanel-preston-7334836", + "review": 0, + "should_download": 0, + "title": "Anal-Nuru-Massage mit Chanel Preston | xHamster", + "file_name": "Anal-Nuru-Massage mit Chanel Preston [7334836].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d1ff3e98-3148-43b0-bc48-3b332a0bb0ff.mp4" + }, + { + "id": "d26f3ac9-1c23-4a06-8c68-ee8d9e27c6f2", + "created_date": "2024-07-25 07:29:45.654680", + "last_modified_date": "2024-07-25 07:29:45.654680", + "version": 0, + "url": "https://ge.xhamster.com/videos/neighborhood-whore-the-drive-in-xhlnc7o", + "review": 0, + "should_download": 0, + "title": "Nachbarschaftshure bei der Einfahrt | xHamster", + "file_name": "Nachbarschaftshure bei der Einfahrt [xhlnc7o].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d26f3ac9-1c23-4a06-8c68-ee8d9e27c6f2.mp4" + }, + { + "id": "d26f8f8a-a819-4f35-94fe-115a2434729e", + "created_date": "2024-07-25 07:29:47.993485", + "last_modified_date": "2024-07-25 07:29:47.993485", + "version": 0, + "url": "https://ge.xhamster.com/videos/barely-legal-5-6-12629903", + "review": 0, + "should_download": 0, + "title": "Kaum legal 5 & 6 | xHamster", + "file_name": "Kaum legal 5 & 6 [12629903].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d26f8f8a-a819-4f35-94fe-115a2434729e.mp4" + }, + { + "id": "d297d126-3dbf-495a-8b4f-26e4b9b8117a", + "created_date": "2024-07-25 07:29:44.405386", + "last_modified_date": "2024-07-25 07:29:44.405386", + "version": 0, + "url": "https://ge.xhamster.com/videos/mommy4k-mommy-does-it-again-xhDFhEs", + "review": 0, + "should_download": 0, + "title": "MOMMY4K. Mama macht es wieder | xHamster", + "file_name": "MOMMY4K. Mama macht es wieder [xhDFhEs].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/d297d126-3dbf-495a-8b4f-26e4b9b8117a.mp4" + }, + { + "id": "d2f21e84-901a-4451-ba9a-089382dd5be9", + "created_date": "2024-07-25 07:29:44.262139", + "last_modified_date": "2024-07-25 07:29:44.262139", + "version": 0, + "url": "https://ge.xhamster.com/videos/taboo-family-the-last-taboo-xhHFfec", + "review": 0, + "should_download": 0, + "title": "Tabu-Familie - das letzte Tabu | xHamster", + "file_name": "Tabu-Familie - das letzte Tabu [xhHFfec].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d2f21e84-901a-4451-ba9a-089382dd5be9.mp4" + }, + { + "id": "d353d233-312f-4048-b7ab-de91de18eb28", + "created_date": "2024-07-25 07:29:45.207752", + "last_modified_date": "2024-07-25 07:29:45.207752", + "version": 0, + "url": "https://ge.xhamster.com/videos/tushy-first-double-penetration-for-redhead-kimberly-brix-5985685", + "review": 0, + "should_download": 0, + "title": "Tushy, erste Doppelpenetration f\u00fcr rothaarige Kimberly Brix | xHamster", + "file_name": "Tushy, erste Doppelpenetration f\u00fcr rothaarige Kimberly Brix [5985685].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d353d233-312f-4048-b7ab-de91de18eb28.mp4" + }, + { + "id": "d368b14b-c61b-4d5e-ae99-557e91749dc7", + "created_date": "2024-07-25 07:29:45.123782", + "last_modified_date": "2024-07-25 07:29:45.123782", + "version": 0, + "url": "https://ge.xhamster.com/videos/game-orgy-xhJtTBz", + "review": 0, + "should_download": 0, + "title": "Spielorgie | xHamster", + "file_name": "Spielorgie [xhJtTBz].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d368b14b-c61b-4d5e-ae99-557e91749dc7.mp4" + }, + { + "id": "d369facc-3ceb-4d1a-adf4-a91bc8e23c52", + "created_date": "2024-07-25 07:29:44.503415", + "last_modified_date": "2024-07-25 07:29:44.503415", + "version": 0, + "url": "https://ge.xhamster.com/videos/two-couples-having-sex-on-a-boat-xhjktWw", + "review": 0, + "should_download": 0, + "title": "Zwei Paare haben Sex auf einem Boot | xHamster", + "file_name": "Zwei Paare haben Sex auf einem Boot [xhjktWw].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d369facc-3ceb-4d1a-adf4-a91bc8e23c52.mp4" + }, + { + "id": "d3a959b2-4c65-43bc-a002-2c3c6bc342ec", + "created_date": "2024-07-25 07:29:45.586939", + "last_modified_date": "2024-07-25 07:29:45.586939", + "version": 0, + "url": "https://ge.xhamster.com/videos/too-much-too-soon-13654213", + "review": 0, + "should_download": 0, + "title": "Zu viel zu fr\u00fch | xHamster", + "file_name": "Zu viel zu fr\u00fch [13654213].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d3a959b2-4c65-43bc-a002-2c3c6bc342ec.mp4" + }, + { + "id": "d3b6a04f-6f86-4861-a198-476b35a9292d", + "created_date": "2024-07-25 07:29:45.154438", + "last_modified_date": "2024-07-25 07:29:45.154438", + "version": 0, + "url": "https://ge.xhamster.com/videos/time-smut-pt-3-a-day-at-the-beach-in-a-freeuse-world-xh5w7CI", + "review": 0, + "should_download": 0, + "title": "Time Smut Teil 3 - ein Tag am Strand in einer freien Welt. | xHamster", + "file_name": "Time Smut Teil 3 - ein Tag am Strand in einer freien Welt. [xh5w7CI].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d3b6a04f-6f86-4861-a198-476b35a9292d.mp4" + }, + { + "id": "d44a9906-26ed-4a9d-a032-38472e45c354", + "created_date": "2024-07-25 07:29:44.487806", + "last_modified_date": "2024-07-25 07:29:44.487806", + "version": 0, + "url": "https://ge.xhamster.com/videos/blonde-double-teamed-by-the-pool-side-xhXKOfx", + "review": 0, + "should_download": 0, + "title": "Blondine doppelt am pool genommen | xHamster", + "file_name": "Blondine doppelt am pool genommen [xhXKOfx].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d44a9906-26ed-4a9d-a032-38472e45c354.mp4" + }, + { + "id": "d4e74556-206c-4f6b-a13c-6f63442a604f", + "created_date": "2024-07-25 07:29:45.745758", + "last_modified_date": "2024-07-25 07:29:45.745758", + "version": 0, + "url": "https://ge.xhamster.com/videos/little-sisters-1972-remastered-7546134", + "review": 0, + "should_download": 0, + "title": "Little Sisters - 1972 (remastered) | xHamster", + "file_name": "Little Sisters - 1972 (remastered) [7546134].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/d4e74556-206c-4f6b-a13c-6f63442a604f.mp4" + }, + { + "id": "d4f43d34-3f3e-46f5-90fb-ab87ead26c7f", + "created_date": "2024-07-25 07:29:47.344326", + "last_modified_date": "2024-07-25 07:29:47.344326", + "version": 0, + "url": "https://ge.xhamster.com/videos/freeuse-stepsisters-are-the-best-ava-sinclaire-and-aften-opal-free-use-fantasy-xhfceZX", + "review": 0, + "should_download": 0, + "title": "Freeuse - Stiefschwestern sind die besten - Ava Sinclaire und oft opalfreie Fantasie | xHamster", + "file_name": "Freeuse - Stiefschwestern sind die besten - Ava Sinclaire und oft opalfreie Fantasie [xhfceZX].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d4f43d34-3f3e-46f5-90fb-ab87ead26c7f.mp4" + }, + { + "id": "d5361e26-3ef5-4254-9748-73f45803856d", + "created_date": "2024-07-25 07:29:46.946349", + "last_modified_date": "2024-07-25 07:29:46.946349", + "version": 0, + "url": "https://ge.xhamster.com/videos/a-highly-dysfunctional-family-xhYDFH4", + "review": 0, + "should_download": 0, + "title": "Eine Familie mit hoher Dysfunktion | xHamster", + "file_name": "Eine Familie mit hoher Dysfunktion [xhYDFH4].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d5361e26-3ef5-4254-9748-73f45803856d.mp4" + }, + { + "id": "d5550ccd-f756-43a3-8db5-53a47a50e5bf", + "created_date": "2024-07-25 07:29:47.741744", + "last_modified_date": "2024-07-25 07:29:47.741744", + "version": 0, + "url": "https://ge.xhamster.com/videos/busty-redhead-wife-syren-de-mer-gets-pounded-by-her-cuckold-husbands-boss-in-front-of-him-mylf-xhzOBv3", + "review": 0, + "should_download": 0, + "title": "Die vollbusige rothaarige ehefrau syren de mer wird vom chef ihres cuckold-ehemanns vor ihm - mylf - geritten | xHamster", + "file_name": "Die vollbusige rothaarige ehefrau syren de mer wird vom chef ihres cuckold-ehemanns vor ihm - mylf - geritten [xhzOBv3].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d5550ccd-f756-43a3-8db5-53a47a50e5bf.mp4" + }, + { + "id": "d5c19748-f124-4d2c-8ca4-b45e471570b5", + "created_date": "2024-07-25 07:29:47.016195", + "last_modified_date": "2024-07-25 07:29:47.016195", + "version": 0, + "url": "https://ge.xhamster.com/videos/hot-italian-mom-fucks-step-sons-best-friend-artemisia-love-xhEcGzf", + "review": 0, + "should_download": 0, + "title": "Hei\u00dfe italienische Mutter fickt den besten Freund des Sohnes - Artemisia Love | xHamster", + "file_name": "Hei\u00dfe italienische Mutter fickt den besten Freund des Sohnes - Artemisia Love [xhEcGzf].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d5c19748-f124-4d2c-8ca4-b45e471570b5.mp4" + }, + { + "id": "d60f7bdd-7fa2-4f77-b5e2-0b7b06e7d5a0", + "created_date": "2024-07-25 07:29:45.270905", + "last_modified_date": "2024-07-25 07:29:45.270905", + "version": 0, + "url": "https://ge.xhamster.com/videos/eighteen-lust-2005-full-movie-10776579", + "review": 0, + "should_download": 0, + "title": "Eighteen Lust (2005) kompletter Film | xHamster", + "file_name": "Eighteen Lust (2005) kompletter Film [10776579].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/d60f7bdd-7fa2-4f77-b5e2-0b7b06e7d5a0.mp4" + }, + { + "id": "d63f6456-3617-4c71-807e-3f75646746de", + "created_date": "2024-12-29 23:53:27.954241", + "last_modified_date": "2024-12-29 23:53:27.954241", + "version": 0, + "url": "https://ge.xhamster.com/videos/insatiable-1980-xhz6KxN", + "review": 0, + "should_download": 0, + "title": "Uners\u00e4ttlich (1980) | xHamster", + "file_name": "Uners\u00e4ttlich (1980) [xhz6KxN].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/d63f6456-3617-4c71-807e-3f75646746de.mp4" + }, + { + "id": "d6888f01-a201-43a0-a45f-85c91174378e", + "created_date": "2024-07-25 07:29:45.846800", + "last_modified_date": "2024-07-25 07:29:45.846800", + "version": 0, + "url": "https://ge.xhamster.com/videos/ok-if-you-lick-something-that-does-not-mean-that-you-own-it-milf-summer-hart-shouts-xhXbfWu", + "review": 0, + "should_download": 0, + "title": "\"Ok, wenn du etwas leckst, hei\u00dft das nicht, dass du es besitzt!\" MILF Summer Hart schreit | xHamster", + "file_name": "\uff02Ok, wenn du etwas leckst, hei\u00dft das nicht, dass du es besitzt!\uff02 MILF Summer Hart schreit [xhXbfWu].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d6888f01-a201-43a0-a45f-85c91174378e.mp4" + }, + { + "id": "d690a959-d2c0-494f-b095-bdf290ed9772", + "created_date": "2024-07-25 07:29:46.565242", + "last_modified_date": "2024-07-25 07:29:46.565242", + "version": 0, + "url": "https://ge.xhamster.com/videos/german-farm-1220162", + "review": 0, + "should_download": 0, + "title": "Deutscher Bauernhof | xHamster", + "file_name": "Deutscher Bauernhof [1220162].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d690a959-d2c0-494f-b095-bdf290ed9772.mp4" + }, + { + "id": "d699d176-3ec4-4258-a5f9-cc1e13c46dd1", + "created_date": "2024-07-25 07:29:44.293115", + "last_modified_date": "2024-07-25 07:29:44.293115", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsis-says-i-didnt-know-stepbro-had-such-a-big-fucking-dick-xhOrCS8", + "review": 0, + "should_download": 0, + "title": "Stiefschwester sagt, ich wusste nicht, dass Stiefbruder einen so gro\u00dfen verdammten Schwanz hatte | xHamster", + "file_name": "Stiefschwester sagt, ich wusste nicht, dass Stiefbruder einen so gro\u00dfen verdammten Schwanz hatte [xhOrCS8].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d699d176-3ec4-4258-a5f9-cc1e13c46dd1.mp4" + }, + { + "id": "d6e77df1-2085-483e-b25f-fcea57e7a2ea", + "created_date": "2024-07-25 07:29:47.292495", + "last_modified_date": "2024-07-25 07:29:47.292495", + "version": 0, + "url": "https://ge.xhamster.com/videos/die-total-versaute-ballettschule-full-german-movie-xhRPuHB", + "review": 0, + "should_download": 0, + "title": "Die Total Versaute Ballettschule- Full German Movie | xHamster", + "file_name": "Die Total Versaute Ballettschule- full german movie [xhRPuHB].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d6e77df1-2085-483e-b25f-fcea57e7a2ea.mp4" + }, + { + "id": "d747134e-3996-4933-aed0-9a610e4d1407", + "created_date": "2024-07-25 07:29:45.789670", + "last_modified_date": "2024-07-25 07:29:45.789670", + "version": 0, + "url": "https://ge.xhamster.com/videos/dinos-first-class-03-road-runner-xhWyDjg", + "review": 0, + "should_download": 0, + "title": "Dinos erste Klasse 03: Stra\u00dfenl\u00e4ufer | xHamster", + "file_name": "Dinos erste Klasse 03\uff1a Stra\u00dfenl\u00e4ufer [xhWyDjg].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d747134e-3996-4933-aed0-9a610e4d1407.mp4" + }, + { + "id": "d769424d-4c99-4c71-927c-a5b2e74de47b", + "created_date": "2024-07-25 07:29:46.584008", + "last_modified_date": "2024-07-25 07:29:46.584008", + "version": 0, + "url": "https://ge.xhamster.com/videos/secrets-1984-xhO45a8", + "review": 0, + "should_download": 0, + "title": "Suss und schamlos - Secrets (1984) | xHamster", + "file_name": "Suss und schamlos - Secrets (1984) [xhO45a8].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d769424d-4c99-4c71-927c-a5b2e74de47b.mp4" + }, + { + "id": "d77a52f6-f233-4d0e-ac03-0ad11f2b15d6", + "created_date": "2024-07-25 07:29:45.782227", + "last_modified_date": "2024-07-25 07:29:45.782227", + "version": 0, + "url": "https://ge.xhamster.com/videos/teeny-gangbang-party-hier-darf-jeder-mal-xhory9I", + "review": 0, + "should_download": 0, + "title": "Teeny Gangbang Party Hier Darf Jeder Mal, Porn 17 | xHamster", + "file_name": "Teeny Gangbang Party hier darf jeder mal [xhory9I].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d77a52f6-f233-4d0e-ac03-0ad11f2b15d6.mp4" + }, + { + "id": "d79d6de4-017e-4e6c-9a9b-e8634163b076", + "created_date": "2024-07-25 07:29:45.851319", + "last_modified_date": "2025-01-03 11:56:48.588000", + "version": 1, + "url": "https://ge.xhamster.com/videos/top-rated-classic-22-4k-restoration-11447106", + "review": 0, + "should_download": 0, + "title": "Bestbewertete klassische 22 - 4k-Restauration | xHamster", + "file_name": "Bestbewertete klassische 22 - 4k-Restauration [11447106].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d79d6de4-017e-4e6c-9a9b-e8634163b076.mp4" + }, + { + "id": "d7b3598d-d684-4aff-94d8-603cf0d2055c", + "created_date": "2024-07-25 07:29:45.476321", + "last_modified_date": "2024-07-25 07:29:45.476321", + "version": 0, + "url": "https://ge.xhamster.com/videos/sluts-getting-facial-after-sucking-dick-and-taking-cocks-in-fuck-holes-in-bus-xhUqETE", + "review": 0, + "should_download": 0, + "title": "Schlampen bekommen Gesichtsbesamung, nachdem sie Schwanz gelutscht haben und Schw\u00e4nze in Fickl\u00f6cher im Bus genommen haben | xHamster", + "file_name": "Schlampen bekommen Gesichtsbesamung, nachdem sie Schwanz gelutscht haben und Schw\u00e4nze in Fickl\u00f6cher im Bus genommen haben [xhUqETE].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/d7b3598d-d684-4aff-94d8-603cf0d2055c.mp4" + }, + { + "id": "d7c54e9b-feba-4ec3-a5fa-fbafba81519b", + "created_date": "2024-09-24 08:11:38.996727", + "last_modified_date": "2024-10-21 16:32:39.982000", + "version": 1, + "url": "https://ge.xhamster.com/videos/stepmom-lets-stepdaughter-jointhreesome-with-shockingly-huge-cock-xhxuLhW", + "review": 0, + "should_download": 0, + "title": "Stiefmutter l\u00e4sst stieftochter zu dreier mit schockierend riesigem schwanz kommen | xHamster", + "file_name": "Stiefmutter l\u00e4sst stieftochter zu dreier mit schockierend riesigem schwanz kommen [xhxuLhW].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/d7c54e9b-feba-4ec3-a5fa-fbafba81519b.mp4" + }, + { + "id": "d7dbb315-ab66-4611-a0bd-59cf032b6a57", + "created_date": "2024-09-24 08:11:38.998949", + "last_modified_date": "2024-10-21 16:32:46.162000", + "version": 1, + "url": "https://ge.xhamster.com/videos/i-want-you-and-your-friend-to-fuck-me-xhD6tWB", + "review": 0, + "should_download": 0, + "title": "Ich will, dass du und dein freund mich ficken! | xHamster", + "file_name": "Ich will, dass du und dein freund mich ficken! [xhD6tWB].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/d7dbb315-ab66-4611-a0bd-59cf032b6a57.mp4" + }, + { + "id": "d80e135f-12ff-41b3-b256-80a4469ce197", + "created_date": "2024-08-08 00:54:26.821675", + "last_modified_date": "2024-08-16 11:04:35.691000", + "version": 1, + "url": "https://ge.xhamster.com/videos/simon-says-take-off-your-pants-liz-jordan-tells-lacy-lennon-and-stepbro-s17-e6-xhPZhDs", + "review": 0, + "should_download": 0, + "title": "\"Simon sagt, zieh deine hose aus\", Liz Jordan sagt Lacy Lennon und stiefbruer - S17: e6 | xHamster", + "file_name": "\uff02Simon sagt, zieh deine hose aus\uff02, Liz Jordan sagt Lacy Lennon und stiefbruer - S17\uff1a e6 [xhPZhDs].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d80e135f-12ff-41b3-b256-80a4469ce197.mp4" + }, + { + "id": "d841d21e-562f-4384-9dc0-ee8112bbd3a9", + "created_date": "2024-07-25 07:29:47.627749", + "last_modified_date": "2024-07-25 07:29:47.627749", + "version": 0, + "url": "https://ge.xhamster.com/videos/kuken-3-full-movie-xhV9Ozo", + "review": 0, + "should_download": 0, + "title": "Kuken 3 Full Movie: Free Big Cock Porn Video f2 | xHamster", + "file_name": "KUKEN 3 (Full Movie) [xhV9Ozo].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d841d21e-562f-4384-9dc0-ee8112bbd3a9.mp4" + }, + { + "id": "d876509b-036b-48fe-ad31-6f011500ffbf", + "created_date": "2024-11-01 21:08:26.934780", + "last_modified_date": "2024-11-01 21:08:26.934780", + "version": 0, + "url": "https://ge.xhamster.com/videos/in-der-scheune-die-notgeile-blonde-gefickt-xhNdWNr", + "review": 0, + "should_download": 0, + "title": "In Der Scheune Die Notgeile Blonde Gefickt: Free HD Porn c4 | xHamster", + "file_name": "In der Scheune die Notgeile Blonde gefickt [xhNdWNr].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/d876509b-036b-48fe-ad31-6f011500ffbf.mp4" + }, + { + "id": "d9591140-4e61-478e-8185-064aef0a9c32", + "created_date": "2024-11-10 16:53:33.469846", + "last_modified_date": "2024-11-10 16:53:33.469846", + "version": 0, + "url": "https://ge.xhamster.com/videos/humiliation-01-4539633", + "review": 0, + "should_download": 0, + "title": "Dem\u00fctigung 01 | xHamster", + "file_name": "Dem\u00fctigung 01 [4539633].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/d9591140-4e61-478e-8185-064aef0a9c32.mp4" + }, + { + "id": "d98f3cd5-7dad-4e28-bc09-83947c3707c9", + "created_date": "2024-10-14 20:33:38.256401", + "last_modified_date": "2024-10-21 16:32:53.696000", + "version": 1, + "url": "https://ge.xhamster.com/videos/la-grande-enfilade-1978-8405888", + "review": 0, + "should_download": 0, + "title": "La Grande Enfilade - 1978, Free Vintage Porn 89 | xHamster", + "file_name": "La Grande Enfilade - 1978 [8405888].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/d98f3cd5-7dad-4e28-bc09-83947c3707c9.mp4" + }, + { + "id": "d9f1d5ad-e13e-4c37-9b1d-c4e4f7fd2797", + "created_date": "2024-07-25 07:29:46.666589", + "last_modified_date": "2024-07-25 07:29:46.666589", + "version": 0, + "url": "https://ge.xhamster.com/videos/anja-und-der-herbergsvater-germany-1998-4518894", + "review": 0, + "should_download": 0, + "title": "Anja Und Der Herbergsvater Germany 1998, Porn 35 | xHamster", + "file_name": "Anja und der Herbergsvater (Germany 1998) [4518894].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/d9f1d5ad-e13e-4c37-9b1d-c4e4f7fd2797.mp4" + }, + { + "id": "da16d6a4-08e7-4ed2-96ad-1b91182b302c", + "created_date": "2024-07-25 07:29:45.583419", + "last_modified_date": "2024-07-25 07:29:45.583419", + "version": 0, + "url": "https://ge.xhamster.com/videos/young-slut-getting-pounded-outdoors-xhaE8my", + "review": 0, + "should_download": 0, + "title": "Junge schlampe wird im freien geritten | xHamster", + "file_name": "Junge schlampe wird im freien geritten [xhaE8my].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/da16d6a4-08e7-4ed2-96ad-1b91182b302c.mp4" + }, + { + "id": "da4cc5be-ce66-4ee9-9328-132cf7cdfce8", + "created_date": "2024-07-25 07:29:45.033009", + "last_modified_date": "2024-07-25 07:29:45.033009", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepmom-says-you-should-really-put-those-tits-away-xhGY2TV", + "review": 0, + "should_download": 0, + "title": "Stiefmutter sagt, du solltest diese Titten wirklich weglegen! | xHamster", + "file_name": "Stiefmutter sagt, du solltest diese Titten wirklich weglegen! [xhGY2TV].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/da4cc5be-ce66-4ee9-9328-132cf7cdfce8.mp4" + }, + { + "id": "da5bbdb5-9746-43ba-83bc-ebc61502f383", + "created_date": "2024-07-25 07:29:47.949494", + "last_modified_date": "2024-07-25 07:29:47.949494", + "version": 0, + "url": "https://ge.xhamster.com/videos/she-loves-making-peepers-horny-they-may-cum-on-her-mouth-xhEfVST", + "review": 0, + "should_download": 0, + "title": "Sie liebt es, Spanner geil zu machen! sie k\u00f6nnen auf ihren Mund kommen | xHamster", + "file_name": "Sie liebt es, Spanner geil zu machen! sie k\u00f6nnen auf ihren Mund kommen [xhEfVST].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/da5bbdb5-9746-43ba-83bc-ebc61502f383.mp4" + }, + { + "id": "da97c0e7-9a66-4ef4-9721-99f87a73a1de", + "created_date": "2024-07-25 07:29:45.178074", + "last_modified_date": "2024-07-25 07:29:45.178074", + "version": 0, + "url": "https://ge.xhamster.com/videos/horny-lexi-luna-cant-resist-her-boyfriend-college-sons-hard-rock-dick-brazzers-xhWqdyJ", + "review": 0, + "should_download": 0, + "title": "Geile lexi luna kann dem harten steinschwanz - brazzers des college-sohns ihres freundes nicht widerstehen | xHamster", + "file_name": "Geile lexi luna kann dem harten steinschwanz - brazzers des college-sohns ihres freundes nicht widerstehen [xhWqdyJ].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/da97c0e7-9a66-4ef4-9721-99f87a73a1de.mp4" + }, + { + "id": "daa6db62-8803-4ef8-894b-4b6a8faa7cc4", + "created_date": "2024-07-25 07:29:47.953245", + "last_modified_date": "2024-07-25 07:29:47.953245", + "version": 0, + "url": "https://ge.xhamster.desi/videos/i-was-surprised-by-my-reaction-to-my-naked-stepsister-xhdXhea", + "review": 0, + "should_download": 0, + "title": "Ich war von meiner Reaktion auf meine nackte Stiefschwester \u00fcberrascht. | xHamster", + "file_name": "Ich war von meiner Reaktion auf meine nackte Stiefschwester \u00fcberrascht. [xhdXhea].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/daa6db62-8803-4ef8-894b-4b6a8faa7cc4.mp4" + }, + { + "id": "dab2b160-3b18-4206-8ea6-9adce974b705", + "created_date": "2024-07-25 07:29:46.144530", + "last_modified_date": "2024-07-25 07:29:46.144530", + "version": 0, + "url": "https://ge.xhamster.com/videos/horny-weekend-1986-10623425", + "review": 0, + "should_download": 0, + "title": "Geiles Wochenende (1986) | xHamster", + "file_name": "Geiles Wochenende (1986) [10623425].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/dab2b160-3b18-4206-8ea6-9adce974b705.mp4" + }, + { + "id": "daf8430b-71c4-4658-a070-018178829033", + "created_date": "2024-10-07 20:47:56.411147", + "last_modified_date": "2024-10-21 16:32:58.411000", + "version": 1, + "url": "https://ge.xhamster.com/videos/fucking-at-work-and-with-friend-and-boss-10788692", + "review": 0, + "should_download": 0, + "title": "Ficken bei der Arbeit und mit Freund und Chef | xHamster", + "file_name": "Ficken bei der Arbeit und mit Freund und Chef [10788692].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/daf8430b-71c4-4658-a070-018178829033.mp4" + }, + { + "id": "db03311d-5e61-4075-ba5a-fc10c5fcd70e", + "created_date": "2024-09-24 08:11:39.001434", + "last_modified_date": "2024-10-21 16:33:04.074000", + "version": 1, + "url": "https://ge.xhamster.com/videos/horny-stepmother-jerks-off-her-husbands-dick-while-her-stepson-fucks-her-in-the-ass-with-his-big-dick-xhf533Q", + "review": 0, + "should_download": 0, + "title": "Geile stiefmutter wichst den schwanz ihres ehemanns, w\u00e4hrend ihr stiefsohn sie mit seinem gro\u00dfen schwanz in den arsch fickt! | xHamster", + "file_name": "Geile stiefmutter wichst den schwanz ihres ehemanns, w\u00e4hrend ihr stiefsohn sie mit seinem gro\u00dfen schwanz in den arsch fickt! [xhf533Q].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/db03311d-5e61-4075-ba5a-fc10c5fcd70e.mp4" + }, + { + "id": "db07f91f-39a1-46ad-8316-7274c14ffdd2", + "created_date": "2024-07-25 07:29:46.983251", + "last_modified_date": "2024-07-25 07:29:46.983251", + "version": 0, + "url": "https://ge.xhamster.com/videos/german-redhead-college-girl-lia-louise-picked-up-for-public-sex-xh02h7A", + "review": 0, + "should_download": 0, + "title": "Deutsche rothaarige Sch\u00fclerin Lia Louise im Wald bei Bochum versaut gefickt | xHamster", + "file_name": "Deutsche rothaarige Sch\u00fclerin Lia Louise im Wald bei Bochum versaut gefickt [xh02h7A].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/db07f91f-39a1-46ad-8316-7274c14ffdd2.mp4" + }, + { + "id": "db3595bd-297f-4aed-bca9-f814cc7c5af3", + "created_date": "2024-07-25 07:29:48.082177", + "last_modified_date": "2024-07-25 07:29:48.082177", + "version": 0, + "url": "https://ge.xhamster.com/videos/fotzenheilanstalt-full-movie-feat-steffi-cerien-5593882", + "review": 0, + "should_download": 0, + "title": "Fotzenheilanstalt Full Movie Feat Steffi & Cerien: Porn 1a | xHamster", + "file_name": "Fotzenheilanstalt. (full movie) feat. Steffi & Cerien [5593882].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/db3595bd-297f-4aed-bca9-f814cc7c5af3.mp4" + }, + { + "id": "dbcd6cfb-956f-48dd-982f-e93f50ee6327", + "created_date": "2024-07-25 07:29:44.469296", + "last_modified_date": "2024-07-25 07:29:44.469296", + "version": 0, + "url": "https://ge.xhamster.com/videos/amber-stark-tells-stepbro-you-better-not-cum-in-my-sticky-panties-s7-e7-xhjaKgI", + "review": 0, + "should_download": 0, + "title": "Amber Stark sagt zu Stiefbruder: \"Du solltest besser nicht in mein klebriges H\u00f6schen kommen\" - s7: e7 | xHamster", + "file_name": "Amber Stark sagt zu Stiefbruder\uff1a \uff02Du solltest besser nicht in mein klebriges H\u00f6schen kommen\uff02 - s7\uff1a e7 [xhjaKgI].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/dbcd6cfb-956f-48dd-982f-e93f50ee6327.mp4" + }, + { + "id": "dc2a3f3b-d6e6-419b-a15b-567028929c2e", + "created_date": "2024-08-09 21:20:39.323741", + "last_modified_date": "2024-08-16 10:32:18.780000", + "version": 1, + "url": "https://ge.xhamster.com/videos/blonde-slut-just-needs-cum-on-her-face-xhYRyTL", + "review": 0, + "should_download": 0, + "title": "Blonde Schlampe braucht einfach Sperma im Gesicht !! | xHamster", + "file_name": "Blonde Schlampe braucht einfach Sperma im Gesicht !! [xhYRyTL].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/dc2a3f3b-d6e6-419b-a15b-567028929c2e.mp4" + }, + { + "id": "dc2eccb3-3f30-4c1e-9791-157aa5d30c70", + "created_date": "2025-01-16 19:59:57.271119", + "last_modified_date": "2025-01-16 19:59:57.271126", + "version": 0, + "url": "https://ge.xhamster.com/videos/best-threesome-double-face-sitting-pussy-licking-and-explosive-climaxes-xhDgb4c", + "review": 0, + "should_download": 0, + "title": "Bester dreier: Doppel facesitting, muschi lecken und explosive H\u00f6hepunkte | xHamster", + "file_name": "Bester dreier\uff1a Doppel facesitting, muschi lecken und explosive H\u00f6hepunkte [xhDgb4c].mp4", + "path": null, + "cloud_link": "/data/media/dc2eccb3-3f30-4c1e-9791-157aa5d30c70.mp4" + }, + { + "id": "dc30a728-43cb-43bc-9c9a-0877add4896d", + "created_date": "2024-07-25 07:29:47.480541", + "last_modified_date": "2024-07-25 07:29:47.480541", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-nymph-gf-and-her-naughty-stepmom-both-fucked-doggystyle-xh8mrUf", + "review": 0, + "should_download": 0, + "title": "Meine Nymphomanin und ihre freche Stiefmutter werden beide im Doggystyle gefickt | xHamster", + "file_name": "Meine Nymphomanin und ihre freche Stiefmutter werden beide im Doggystyle gefickt [xh8mrUf].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/dc30a728-43cb-43bc-9c9a-0877add4896d.mp4" + }, + { + "id": "dc33c35b-0aed-4ad4-b469-2d56d8c4c138", + "created_date": "2024-07-25 07:29:46.056997", + "last_modified_date": "2024-07-25 07:29:46.056997", + "version": 0, + "url": "https://ge.xhamster.com/videos/perving-on-my-friend-again-no-im-just-making-a-sandwich-13460999", + "review": 0, + "should_download": 0, + "title": "Geiferst du wieder meiner Freundin nach? Nein, ich mache nur ein Sandwich | xHamster", + "file_name": "Geiferst du wieder meiner Freundin nach\uff1f Nein, ich mache nur ein Sandwich [13460999].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/dc33c35b-0aed-4ad4-b469-2d56d8c4c138.mp4" + }, + { + "id": "dcaa919a-9011-4b0c-8698-e41cd9dfb0ce", + "created_date": "2024-07-25 07:29:46.854606", + "last_modified_date": "2024-07-25 07:29:46.854606", + "version": 0, + "url": "https://ge.xhamster.com/videos/die-nichten-der-frau-oberst-1968-german-soft-full-2k-xhc1vdT", + "review": 0, + "should_download": 0, + "title": "Die Nichten Der Frau Oberst 1968 German Soft Full 2k | xHamster", + "file_name": "Die Nichten der Frau Oberst (1968, German soft, full, 2K) [xhc1vdT].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/dcaa919a-9011-4b0c-8698-e41cd9dfb0ce.mp4" + }, + { + "id": "dcaeafae-9a49-4711-b08a-dc6f9422c00b", + "created_date": "2024-07-25 07:29:44.722230", + "last_modified_date": "2024-07-25 07:29:44.722230", + "version": 0, + "url": "https://ge.xhamster.com/videos/school-class-10581008", + "review": 0, + "should_download": 0, + "title": "Schulklasse | xHamster", + "file_name": "Schulklasse [10581008].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/dcaeafae-9a49-4711-b08a-dc6f9422c00b.mp4" + }, + { + "id": "dcb805b6-61a2-4e1d-a49d-3a0539391134", + "created_date": "2024-07-25 07:29:45.621368", + "last_modified_date": "2024-07-25 07:29:45.621368", + "version": 0, + "url": "https://ge.xhamster.com/videos/inz-mach-mit-12096154", + "review": 0, + "should_download": 0, + "title": "Inz Mach Mit: Free Kissing Porn Video 46 | xHamster", + "file_name": "inz mach mit [12096154].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/dcb805b6-61a2-4e1d-a49d-3a0539391134.mp4" + }, + { + "id": "dcbac5cc-3cd0-4fbb-8b29-4375e3e52384", + "created_date": "2024-07-25 07:29:44.417084", + "last_modified_date": "2024-07-25 07:29:44.417084", + "version": 0, + "url": "https://ge.xhamster.com/videos/zeltplatz-wilde-lust-10882784", + "review": 0, + "should_download": 0, + "title": "Zeltplatz Wilde Lust: Free HD Porn Video 8b | xHamster", + "file_name": "Zeltplatz Wilde Lust [10882784].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/dcbac5cc-3cd0-4fbb-8b29-4375e3e52384.mp4" + }, + { + "id": "dcfee43f-43bb-4875-a996-4701b46e4895", + "created_date": "2025-01-16 20:00:01.035133", + "last_modified_date": "2025-01-16 20:00:01.035140", + "version": 0, + "url": "https://ge.xhamster.com/videos/first-nudity-than-artwork-losing-a-bet-gets-sticky-quick-12126741", + "review": 0, + "should_download": 0, + "title": "Erste Nacktheit als Kunstwerk, der Verlust einer Wette wird schnell klebrig | xHamster", + "file_name": "Erste Nacktheit als Kunstwerk, der Verlust einer Wette wird schnell klebrig [12126741].mp4", + "path": null, + "cloud_link": "/data/media/dcfee43f-43bb-4875-a996-4701b46e4895.mp4" + }, + { + "id": "dd31ba36-a7d5-45a1-ac0c-7e801cd49f68", + "created_date": "2024-10-21 15:08:43.553991", + "last_modified_date": "2024-10-21 16:33:12.056000", + "version": 1, + "url": "https://ge.xhamster.com/videos/more-of-this-old-man-fuck-bitch-w-friend-4909408", + "review": 0, + "should_download": 0, + "title": "Mehr von dieser alten Fick-Schlampe mit Freund | xHamster", + "file_name": "Mehr von dieser alten Fick-Schlampe mit Freund [4909408].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/dd31ba36-a7d5-45a1-ac0c-7e801cd49f68.mp4" + }, + { + "id": "dd3fde41-89cf-4d08-abb7-19d9b56fcc94", + "created_date": "2024-10-07 20:47:56.419311", + "last_modified_date": "2024-10-21 16:33:15.525000", + "version": 1, + "url": "https://ge.xhamster.com/videos/shared-wife-with-friends-10011525", + "review": 0, + "should_download": 0, + "title": "Geteilte Ehefrau mit Freunden | xHamster", + "file_name": "Geteilte Ehefrau mit Freunden [10011525].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/dd3fde41-89cf-4d08-abb7-19d9b56fcc94.mp4" + }, + { + "id": "dd625d42-a7a9-411a-a35f-e7855b322edc", + "created_date": "2024-07-25 07:29:46.264444", + "last_modified_date": "2024-07-25 07:29:46.264444", + "version": 0, + "url": "https://ge.xhamster.com/videos/surprising-my-stepmom-with-an-anal-threesome-tabooheat-xhbUaSP", + "review": 0, + "should_download": 0, + "title": "\u00dcberrascht meine Stiefmutter mit einem analen Dreier! - Tabu-Hitze | xHamster", + "file_name": "\u00dcberrascht meine Stiefmutter mit einem analen Dreier! - Tabu-Hitze [xhbUaSP].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/dd625d42-a7a9-411a-a35f-e7855b322edc.mp4" + }, + { + "id": "dd7652cf-f89f-4764-9d6f-f9cb6dff761c", + "created_date": "2024-08-09 21:34:58.067626", + "last_modified_date": "2024-08-16 10:32:33.303000", + "version": 1, + "url": "https://ge.xhamster.com/videos/secretariat-prive-1980-france-elisabeth-bure-full-movie-xhFvnkF", + "review": 0, + "should_download": 0, + "title": "Secretariat Prive (1980, Frankreich, Elizabeth Bure, kompletter Film) | xHamster", + "file_name": "Secretariat Prive (1980, Frankreich, Elizabeth Bure, kompletter Film) [xhFvnkF].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/dd7652cf-f89f-4764-9d6f-f9cb6dff761c.mp4" + }, + { + "id": "dd7b5467-e944-4766-9cf9-1ad5e804c264", + "created_date": "2024-07-25 07:29:46.390960", + "last_modified_date": "2024-07-25 07:29:46.390960", + "version": 0, + "url": "https://ge.xhamster.com/videos/schulmadchen-10-kleine-rosetten-zaghaft-entjungfert-1994-xhQRCB6", + "review": 0, + "should_download": 0, + "title": "Schulmadchen 10 Kleine Rosetten Zaghaft Entjungfert 1994 | xHamster", + "file_name": "Schulmadchen 10 Kleine Rosetten Zaghaft Entjungfert (1994) [xhQRCB6].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/dd7b5467-e944-4766-9cf9-1ad5e804c264.mp4" + }, + { + "id": "dde0b3aa-6857-41ab-81d0-861c3afb5c69", + "created_date": "2024-09-24 08:11:38.996081", + "last_modified_date": "2024-10-21 16:33:21.158000", + "version": 1, + "url": "https://ge.xhamster.com/videos/lustful-couple-finally-convince-babysitter-to-play-with-them-xhk5RyP", + "review": 0, + "should_download": 0, + "title": "Lustvolles Paar \u00fcberzeugt Babysitter endlich, mit ihnen zu spielen | xHamster", + "file_name": "Lustvolles Paar \u00fcberzeugt Babysitter endlich, mit ihnen zu spielen [xhk5RyP].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/dde0b3aa-6857-41ab-81d0-861c3afb5c69.mp4" + }, + { + "id": "dded0bc8-229a-4433-a997-8f7af361625d", + "created_date": "2024-11-10 16:53:33.478632", + "last_modified_date": "2024-11-10 16:53:33.478632", + "version": 0, + "url": "https://ge.xhamster.com/videos/mai-68-en-69-7862418", + "review": 0, + "should_download": 0, + "title": "Mai 68 En 69: Free Big Tits Porn Video ad | xHamster", + "file_name": "mai 68 en 69 [7862418].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/dded0bc8-229a-4433-a997-8f7af361625d.mp4" + }, + { + "id": "de237a86-309b-4d30-80ed-e1d6f6140e18", + "created_date": "2024-07-25 07:29:44.507124", + "last_modified_date": "2024-07-25 07:29:44.507124", + "version": 0, + "url": "https://ge.xhamster.com/videos/little-18-step-sister-gets-multiple-cumshots-on-her-face-by-her-three-step-brothers-xh4XV7j", + "review": 0, + "should_download": 0, + "title": "Little 18 Step Sister gets Multiple Cumshots on Her Face by Her Three Step Brothers | xHamster", + "file_name": "Little 18+ Step Sister gets Multiple cumshots on her face by her Three Step Brothers [xh4XV7j].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/de237a86-309b-4d30-80ed-e1d6f6140e18.mp4" + }, + { + "id": "de83ba7b-8d81-4a39-b673-6b509da2a982", + "created_date": "2024-07-25 07:29:47.968628", + "last_modified_date": "2024-07-25 07:29:47.968628", + "version": 0, + "url": "https://ge.xhamster.com/videos/brunette-slut-gives-a-wet-blowjob-and-her-wet-pussy-to-a-studs-cock-in-the-pool-xhJmW95", + "review": 0, + "should_download": 0, + "title": "Br\u00fcnette br\u00fcnette schlampe gibt dem schwanz eines hengstes im pool einen nassen blowjob und ihre nasse muschi | xHamster", + "file_name": "Br\u00fcnette br\u00fcnette schlampe gibt dem schwanz eines hengstes im pool einen nassen blowjob und ihre nasse muschi [xhJmW95].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/de83ba7b-8d81-4a39-b673-6b509da2a982.mp4" + }, + { + "id": "deba75a9-f21a-4cd4-a73f-04f18cca16f3", + "created_date": "2025-01-16 19:59:43.332731", + "last_modified_date": "2025-01-16 19:59:43.332738", + "version": 0, + "url": "https://ge.xhamster.com/videos/four-girls-are-enjoying-stripping-in-the-swimming-pool-with-one-lucky-guy-joining-them-xhL4uHO", + "review": 0, + "should_download": 0, + "title": "Vier m\u00e4dchen genie\u00dfen es, im schwimmbad zu, zu denen ein gl\u00fcckspilz geh\u00f6rt | xHamster", + "file_name": "Vier m\u00e4dchen genie\u00dfen es, im schwimmbad zu, zu denen ein gl\u00fcckspilz geh\u00f6rt [xhL4uHO].mp4", + "path": null, + "cloud_link": "/data/media/deba75a9-f21a-4cd4-a73f-04f18cca16f3.mp4" + }, + { + "id": "df260cf5-587e-47ae-a9cb-22fbe9e8729d", + "created_date": "2024-12-29 23:53:27.932560", + "last_modified_date": "2024-12-29 23:53:27.932560", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-family-is-different-xhjwGUC", + "review": 0, + "should_download": 0, + "title": "Meine Familie ist anders | xHamster", + "file_name": "Meine Familie ist anders [xhjwGUC].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/df260cf5-587e-47ae-a9cb-22fbe9e8729d.mp4" + }, + { + "id": "df911d60-3709-44d3-8c7b-b9b2d4a72451", + "created_date": "2024-07-25 07:29:44.435712", + "last_modified_date": "2024-07-25 07:29:44.435712", + "version": 0, + "url": "https://ge.xhamster.com/videos/eine-schrecklich-geile-familie-3-1840791", + "review": 0, + "should_download": 0, + "title": "Eine Schrecklich Geile Familie 3, Free Porn 09 | xHamster", + "file_name": "Eine Schrecklich Geile Familie 3 [1840791].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/df911d60-3709-44d3-8c7b-b9b2d4a72451.mp4" + }, + { + "id": "dfccb432-5002-42cb-a20e-889b4d780ecb", + "created_date": "2024-07-25 07:29:45.749353", + "last_modified_date": "2024-07-25 07:29:45.749353", + "version": 0, + "url": "https://ge.xhamster.com/videos/horny-step-son-fucks-step-mom-sister-dava-foxx-kara-lee-14884943", + "review": 0, + "should_download": 0, + "title": "Geiler Stiefsohn fickt Stiefmutter & Stiefschwester - Dava Foxx & Kara Lee | xHamster", + "file_name": "Geiler Stiefsohn fickt Stiefmutter & Stiefschwester - Dava Foxx & Kara Lee [14884943].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/dfccb432-5002-42cb-a20e-889b4d780ecb.mp4" + }, + { + "id": "e001a4ff-2acf-464f-a44b-904578158c73", + "created_date": "2024-07-25 07:29:44.692168", + "last_modified_date": "2024-07-25 07:29:44.692168", + "version": 0, + "url": "https://ge.xhamster.com/videos/inzt-die-schwestern-sind-los-10538086", + "review": 0, + "should_download": 0, + "title": "Inzt Die Schwestern Sind Los, Free Retro Porn 8a | xHamster", + "file_name": "Inzt Die Schwestern sind los [10538086].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/e001a4ff-2acf-464f-a44b-904578158c73.mp4" + }, + { + "id": "e017f51c-2e56-41a7-8819-791fb26433d6", + "created_date": "2024-07-25 07:29:44.942649", + "last_modified_date": "2024-07-25 07:29:44.942649", + "version": 0, + "url": "https://ge.xhamster.com/videos/german-holiday-group-sex-on-pool-5768372", + "review": 0, + "should_download": 0, + "title": "Deutscher Ferien-Gruppensex am Pool | xHamster", + "file_name": "Deutscher Ferien-Gruppensex am Pool [5768372].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e017f51c-2e56-41a7-8819-791fb26433d6.mp4" + }, + { + "id": "e04b707f-524c-4573-8e3d-40bf13dcfafc", + "created_date": "2024-07-25 07:29:44.981925", + "last_modified_date": "2024-07-25 07:29:44.981925", + "version": 0, + "url": "https://ge.xhamster.com/videos/aria-valencia-starts-a-swap-family-fuck-fest-stuck-in-a-basket-s7-e2-xhhtTAN", + "review": 0, + "should_download": 0, + "title": "Aria Valencia beginnt ein Familien-Fickfest, w\u00e4hrend sie in einem Korb steckt - s7: e2 | xHamster", + "file_name": "Aria Valencia beginnt ein Familien-Fickfest, w\u00e4hrend sie in einem Korb steckt - s7\uff1a e2 [xhhtTAN].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e04b707f-524c-4573-8e3d-40bf13dcfafc.mp4" + }, + { + "id": "e05bcd0d-b49f-4935-8614-0626a3b1472a", + "created_date": "2024-07-25 07:29:46.340349", + "last_modified_date": "2024-07-25 07:29:46.340349", + "version": 0, + "url": "https://ge.xhamster.com/videos/tabulose-familie-without-taboos-xhCRgoB", + "review": 0, + "should_download": 0, + "title": "Tabulose Familie Without Taboos, Free Porn 6f | xHamster", + "file_name": "Tabulose Familie without taboos [xhCRgoB].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e05bcd0d-b49f-4935-8614-0626a3b1472a.mp4" + }, + { + "id": "e0662903-dcac-4c3d-a207-286e54527afb", + "created_date": "2024-07-25 07:29:47.738060", + "last_modified_date": "2024-07-25 07:29:47.738060", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-dick-definitely-wouldnt-fit-in-this-pussy-redneck-fuck-12977014", + "review": 0, + "should_download": 0, + "title": "Mein Schwanz w\u00fcrde definitiv nicht in diesen Muschi-Redneck-Fick passen | xHamster", + "file_name": "Mein Schwanz w\u00fcrde definitiv nicht in diesen Muschi-Redneck-Fick passen [12977014].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e0662903-dcac-4c3d-a207-286e54527afb.mp4" + }, + { + "id": "e09ee61e-55a2-44ee-8b6f-a9d935be421c", + "created_date": "2024-07-25 07:29:46.213232", + "last_modified_date": "2024-07-25 07:29:46.213232", + "version": 0, + "url": "https://ge.xhamster.com/videos/psychology-of-the-orgasm-1970-german-classic-xhLeVL0", + "review": 0, + "should_download": 0, + "title": "Psychologie des Orgasmus (1970) - deutscher Klassiker | xHamster", + "file_name": "Psychologie des Orgasmus (1970) - deutscher Klassiker [xhLeVL0].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e09ee61e-55a2-44ee-8b6f-a9d935be421c.mp4" + }, + { + "id": "e0a77d6e-9f8e-4161-872e-f38b8d5dd78b", + "created_date": "2024-11-10 16:53:33.467595", + "last_modified_date": "2024-11-10 16:53:33.467595", + "version": 0, + "url": "https://ge.xhamster.com/videos/french-holliday-1982-12159589", + "review": 0, + "should_download": 0, + "title": "French Holliday (1982) | xHamster", + "file_name": "French Holliday (1982) [12159589].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/e0a77d6e-9f8e-4161-872e-f38b8d5dd78b.mp4" + }, + { + "id": "e1358a6f-1a63-4aa0-a3e0-45105e2665b4", + "created_date": "2024-07-25 07:29:44.307766", + "last_modified_date": "2024-07-25 07:29:44.307766", + "version": 0, + "url": "https://ge.xhamster.com/videos/insatiable-ep-02-xhFJKYl", + "review": 0, + "should_download": 0, + "title": "Uners\u00e4ttlich Ep 02 | xHamster", + "file_name": "Uners\u00e4ttlich Ep 02 [xhFJKYl].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e1358a6f-1a63-4aa0-a3e0-45105e2665b4.mp4" + }, + { + "id": "e15bfccb-1530-4904-b83e-e5ba1c9530fe", + "created_date": "2024-07-25 07:29:44.990117", + "last_modified_date": "2024-07-25 07:29:44.990117", + "version": 0, + "url": "https://ge.xhamster.com/videos/spicy-roulette-video-with-3-teens-fucking-6366442", + "review": 0, + "should_download": 0, + "title": "W\u00fcrziges Roulette-Video mit 3 Teenagern, die ficken | xHamster", + "file_name": "W\u00fcrziges Roulette-Video mit 3 Teenagern, die ficken [6366442].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e15bfccb-1530-4904-b83e-e5ba1c9530fe.mp4" + }, + { + "id": "e1a0a601-2d74-47a6-9a0b-6a2821d06a99", + "created_date": "2024-07-25 07:29:46.060581", + "last_modified_date": "2024-07-25 07:29:46.060581", + "version": 0, + "url": "https://ge.xhamster.com/videos/private-scandals-3-full-movie-xh4ZBPi", + "review": 0, + "should_download": 0, + "title": "Private Skandale 3 (kompletter Film) | xHamster", + "file_name": "Private Skandale 3 (kompletter Film) [xh4ZBPi].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e1a0a601-2d74-47a6-9a0b-6a2821d06a99.mp4" + }, + { + "id": "e1b6cff6-844a-4024-a1f7-f44d872bc891", + "created_date": "2024-12-30 18:57:55.730000", + "last_modified_date": "2025-01-03 01:46:07.676000", + "version": 2, + "url": "https://ge.xhamster.com/videos/18-videoz-izi-ashley-team-fuck-with-anal-and-facial-10493078", + "review": 0, + "should_download": 0, + "title": "18 Videoz - Izi Ashley - Team-Fick mit Anal und Gesichtsbesamung | xHamster", + "file_name": "18 Videoz - Izi Ashley - Team-Fick mit Anal und Gesichtsbesamung [10493078].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/e1b6cff6-844a-4024-a1f7-f44d872bc891.mp4" + }, + { + "id": "e221801a-7c00-4c48-98b3-2b538a6799b6", + "created_date": "2024-12-29 23:53:27.955457", + "last_modified_date": "2024-12-29 23:53:27.955457", + "version": 0, + "url": "https://ge.xhamster.com/videos/stiff-competition-1984-9264206", + "review": 0, + "should_download": 0, + "title": "Harter Wettbewerb (1984) | xHamster", + "file_name": "Harter Wettbewerb (1984) [9264206].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/e221801a-7c00-4c48-98b3-2b538a6799b6.mp4" + }, + { + "id": "e2702fcd-a125-412a-a7b4-7e20999a67ed", + "created_date": "2024-07-25 07:29:46.877857", + "last_modified_date": "2024-07-25 07:29:46.877857", + "version": 0, + "url": "https://ge.xhamster.com/videos/step-father-step-son-fuck-teen-daughter-family-therapy-xhaDcLo", + "review": 0, + "should_download": 0, + "title": "Stiefvater & Stiefsohn ficken Teen-Tochter - Familientherapie | xHamster", + "file_name": "Stiefvater & Stiefsohn ficken Teen-Tochter - Familientherapie [xhaDcLo].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e2702fcd-a125-412a-a7b4-7e20999a67ed.mp4" + }, + { + "id": "e28e27df-08cb-47ba-abb7-8eb2fba5795d", + "created_date": "2024-07-25 07:29:47.066989", + "last_modified_date": "2024-07-25 07:29:47.066989", + "version": 0, + "url": "https://ge.xhamster.com/videos/nude-pool-party-at-villa-in-pattaya-amateur-russian-couple-xhyZzL8", + "review": 0, + "should_download": 0, + "title": "Nackte Poolparty in der Villa in Pattaya - russisches amateur-paar | xHamster", + "file_name": "Nackte Poolparty in der Villa in Pattaya - russisches amateur-paar [xhyZzL8].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e28e27df-08cb-47ba-abb7-8eb2fba5795d.mp4" + }, + { + "id": "e2923a57-1772-4ac6-86bd-92a16098ad8b", + "created_date": "2024-07-25 07:29:47.729597", + "last_modified_date": "2024-07-25 07:29:47.729597", + "version": 0, + "url": "https://ge.xhamster.com/videos/best-fucking-vacation-ever-xhMXVjz", + "review": 0, + "should_download": 0, + "title": "Bester Fickurlaub aller Zeiten! | xHamster", + "file_name": "Bester Fickurlaub aller Zeiten! [xhMXVjz].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e2923a57-1772-4ac6-86bd-92a16098ad8b.mp4" + }, + { + "id": "e312cb97-6b0d-4c0a-a027-0263b8df0c8a", + "created_date": "2024-07-25 07:29:46.791938", + "last_modified_date": "2024-07-25 07:29:46.791938", + "version": 0, + "url": "https://ge.xhamster.com/videos/pool-party-turns-into-an-orgy-party-f70-120350", + "review": 0, + "should_download": 0, + "title": "Pool-Party wird zu einer Orgie-Party! f70 | xHamster", + "file_name": "Pool-Party wird zu einer Orgie-Party! f70 [120350].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e312cb97-6b0d-4c0a-a027-0263b8df0c8a.mp4" + }, + { + "id": "e318c11b-19ef-49cc-b435-b444214d5f74", + "created_date": "2024-07-25 07:29:47.686525", + "last_modified_date": "2024-07-25 07:29:47.686525", + "version": 0, + "url": "https://ge.xhamster.com/videos/busty-amateur-wife-gets-gangbanged-by-husbands-friends-14757457", + "review": 0, + "should_download": 0, + "title": "Vollbusige Amateur-Ehefrau wird von den Freunden des Ehemanns im Gangbang gefickt | xHamster", + "file_name": "Vollbusige Amateur-Ehefrau wird von den Freunden des Ehemanns im Gangbang gefickt [14757457].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e318c11b-19ef-49cc-b435-b444214d5f74.mp4" + }, + { + "id": "e32badf9-3c89-4652-a73d-984135ab88b1", + "created_date": "2024-08-28 23:21:54.367087", + "last_modified_date": "2024-08-28 23:21:54.367087", + "version": 0, + "url": "https://ge.xhamster.com/videos/lacy-lennons-first-lesbian-fisting-summer-camp-experience-xhqzGqO", + "review": 0, + "should_download": 0, + "title": "Lacy Lennons erstes lesbisches Fisting-Sommercamp-Erlebnis | xHamster", + "file_name": "Lacy Lennons erstes lesbisches Fisting-Sommercamp-Erlebnis [xhqzGqO].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/e32badf9-3c89-4652-a73d-984135ab88b1.mp4" + }, + { + "id": "e3c206f7-6208-472d-8d08-e716b1f366db", + "created_date": "2024-07-25 07:29:44.300116", + "last_modified_date": "2024-07-25 07:29:44.300116", + "version": 0, + "url": "https://ge.xhamster.com/videos/puremature-seductive-step-mom-alison-star-gets-banged-on-romantic-1291641", + "review": 0, + "should_download": 0, + "title": "Puremature - verf\u00fchrerische Stiefmutter Alison Star wird romantisch geknallt | xHamster", + "file_name": "Puremature - verf\u00fchrerische Stiefmutter Alison Star wird romantisch geknallt [1291641].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e3c206f7-6208-472d-8d08-e716b1f366db.mp4" + }, + { + "id": "e3c4ee26-a23b-4f49-b9cb-3354f2175dac", + "created_date": "2024-07-25 07:29:48.094331", + "last_modified_date": "2024-07-25 07:29:48.094331", + "version": 0, + "url": "https://ge.xhamster.com/videos/strand-orgie-1993-xhXGl0Xv", + "review": 0, + "should_download": 0, + "title": "Strand Orgie 1993: Free European Porn Video 2b | xHamster", + "file_name": "Strand Orgie (1993) [xhXGl0Xv].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e3c4ee26-a23b-4f49-b9cb-3354f2175dac.mp4" + }, + { + "id": "e4452278-d7a3-4aa1-bbe3-dbd55346ee1c", + "created_date": "2024-07-25 07:29:46.437541", + "last_modified_date": "2024-07-25 07:29:46.437541", + "version": 0, + "url": "https://ge.xhamster.com/videos/newly-hatched-chicks-are-ready-to-be-fucked-13508686", + "review": 0, + "should_download": 0, + "title": "Die megageile K\u00fcken-Farm | xHamster", + "file_name": "Die megageile K\u00fcken-Farm [13508686].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e4452278-d7a3-4aa1-bbe3-dbd55346ee1c.mp4" + }, + { + "id": "e47c4efd-004c-49f2-9058-ff07524026f5", + "created_date": "2024-07-25 07:29:44.574377", + "last_modified_date": "2024-07-25 07:29:44.574377", + "version": 0, + "url": "https://ge.xhamster.com/videos/best-friends-are-fucking-for-first-time-after-wife-s-approval-xhtnL0F", + "review": 0, + "should_download": 0, + "title": "Beste Freundinnen ficken zum ersten Mal nach Zustimmung der Ehefrau | xHamster", + "file_name": "Beste Freundinnen ficken zum ersten Mal nach Zustimmung der Ehefrau [xhtnL0F].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e47c4efd-004c-49f2-9058-ff07524026f5.mp4" + }, + { + "id": "e4a850aa-64fc-44e6-8793-b6a058f3313c", + "created_date": "2024-07-25 07:29:45.218685", + "last_modified_date": "2024-07-25 07:29:45.218685", + "version": 0, + "url": "https://ge.xhamster.com/videos/6-hot-girls-in-party-game-341932", + "review": 0, + "should_download": 0, + "title": "6 hei\u00dfe M\u00e4dchen im Partyspiel | xHamster", + "file_name": "6 hei\u00dfe M\u00e4dchen im Partyspiel [341932].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e4a850aa-64fc-44e6-8793-b6a058f3313c.mp4" + }, + { + "id": "e502ee3c-7a8f-490a-9f08-5d506d37068b", + "created_date": "2024-07-25 07:29:45.188855", + "last_modified_date": "2024-07-25 07:29:45.188855", + "version": 0, + "url": "https://ge.xhamster.com/videos/itll-get-much-bigger-if-you-tug-on-it-xhAmxS5", + "review": 0, + "should_download": 0, + "title": "Es wird viel gr\u00f6\u00dfer, wenn du daran ziehst! | xHamster", + "file_name": "Es wird viel gr\u00f6\u00dfer, wenn du daran ziehst! [xhAmxS5].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e502ee3c-7a8f-490a-9f08-5d506d37068b.mp4" + }, + { + "id": "e561b190-63f6-48eb-95db-d81279d16bb6", + "created_date": "2024-07-25 07:29:45.509393", + "last_modified_date": "2024-07-25 07:29:45.509393", + "version": 0, + "url": "https://ge.xhamster.com/videos/among-the-greatest-porn-films-ever-made-179-15006023", + "review": 0, + "should_download": 0, + "title": "Einer der gr\u00f6\u00dften Pornofilme aller Zeiten 179 | xHamster", + "file_name": "Einer der gr\u00f6\u00dften Pornofilme aller Zeiten 179 [15006023].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/e561b190-63f6-48eb-95db-d81279d16bb6.mp4" + }, + { + "id": "e56bbdfe-fa48-4756-b1f5-aaeb34185468", + "created_date": "2024-07-25 07:29:47.581853", + "last_modified_date": "2024-07-25 07:29:47.581853", + "version": 0, + "url": "https://ge.xhamster.com/videos/die-unbeugsame-xh9TsYf", + "review": 0, + "should_download": 0, + "title": "Das Unbemiteliche | xHamster", + "file_name": "Das Unbemiteliche [xh9TsYf].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e56bbdfe-fa48-4756-b1f5-aaeb34185468.mp4" + }, + { + "id": "e56ebcd3-df33-4f00-95fb-d0cd7fc1fdfc", + "created_date": "2024-07-25 07:29:46.833603", + "last_modified_date": "2024-07-25 07:29:46.833603", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-girlfriends-dad-fucked-me-hard-and-came-in-my-ass-to-teach-me-how-to-do-it-with-my-boyfriend-xhSs2E5", + "review": 0, + "should_download": 0, + "title": "Vater der freundin fickte sie hart und kam in ihren arsch, um mich zu lehren, wie man es mit ihrem freund macht. | xHamster", + "file_name": "Vater der freundin fickte sie hart und kam in ihren arsch, um mich zu lehren, wie man es mit ihrem freund macht. [xhSs2E5].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e56ebcd3-df33-4f00-95fb-d0cd7fc1fdfc.mp4" + }, + { + "id": "e57df7ea-3a47-48e5-90a0-e206f3aa398e", + "created_date": "2024-07-25 07:29:46.561021", + "last_modified_date": "2024-07-25 07:29:46.561021", + "version": 0, + "url": "https://ge.xhamster.com/videos/big-titty-18-year-old-fresh-teen-vs-college-thick-cock-bro-9306643", + "review": 0, + "should_download": 0, + "title": "Dicke Titten, 18 Jahre alt, frisches Teen gegen College, dicker Schwanz, Bruder | xHamster", + "file_name": "Dicke Titten, 18 Jahre alt, frisches Teen gegen College, dicker Schwanz, Bruder [9306643].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e57df7ea-3a47-48e5-90a0-e206f3aa398e.mp4" + }, + { + "id": "e59406ee-502a-489d-a8ca-e44e9839a556", + "created_date": "2024-07-25 07:29:44.495727", + "last_modified_date": "2024-07-25 07:29:44.495727", + "version": 0, + "url": "https://ge.xhamster.com/videos/foxy-lady-11-1988-germany-english-live-sound-full-hd-xhMfNUr", + "review": 0, + "should_download": 0, + "title": "Foxy Lady # 11 (1988, Deutschland, englischer Live-Sound, voll, hd) | xHamster", + "file_name": "Foxy Lady # 11 (1988, Deutschland, englischer Live-Sound, voll, hd) [xhMfNUr].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e59406ee-502a-489d-a8ca-e44e9839a556.mp4" + }, + { + "id": "e5a0bd24-d317-4446-b0bb-77185b2f6239", + "created_date": "2024-08-16 12:20:55.081000", + "last_modified_date": "2024-08-16 12:20:55.081000", + "version": 0, + "url": "https://ge.xhamster.com/videos/mom-step-dad-daughter-and-boy-friend-12581760", + "review": 0, + "should_download": 0, + "title": "Mutter, Stiefvater, Tochter und Freund | xHamster", + "file_name": "Mutter, Stiefvater, Tochter und Freund [12581760].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/e5a0bd24-d317-4446-b0bb-77185b2f6239.mp4" + }, + { + "id": "e5acacad-9ccf-4e63-a125-65745401229a", + "created_date": "2024-12-29 23:53:27.902052", + "last_modified_date": "2024-12-29 23:53:27.902052", + "version": 0, + "url": "https://ge.xhamster.com/videos/petite-tattooed-babe-fucked-in-the-office-xh18KRX", + "review": 0, + "should_download": 0, + "title": "Zierliches t\u00e4towiertes sch\u00e4tzchen im b\u00fcro gefickt | xHamster", + "file_name": "Zierliches t\u00e4towiertes sch\u00e4tzchen im b\u00fcro gefickt [xh18KRX].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/e5acacad-9ccf-4e63-a125-65745401229a.mp4" + }, + { + "id": "e5b774e7-3ce7-4032-b268-be1a5adda4e1", + "created_date": "2024-11-10 16:53:33.475766", + "last_modified_date": "2024-11-10 16:53:33.475766", + "version": 0, + "url": "https://ge.xhamster.com/videos/i-am-always-ready-for-group-sex-1978-13175752", + "review": 0, + "should_download": 0, + "title": "Ich bin immer bereit f\u00fcr Gruppensex (1978) | xHamster", + "file_name": "Ich bin immer bereit f\u00fcr Gruppensex (1978) [13175752].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/e5b774e7-3ce7-4032-b268-be1a5adda4e1.mp4" + }, + { + "id": "e5bfbada-d059-4488-b043-98f3478a8a70", + "created_date": "2024-07-25 07:29:45.737577", + "last_modified_date": "2024-07-25 07:29:45.737577", + "version": 0, + "url": "https://ge.xhamster.com/videos/familystrokes-perv-stepmom-and-stepdad-expose-their-naughty-stepdaughter-jessae-rosaes-sex-secret-xhNjs2X", + "review": 0, + "should_download": 0, + "title": "FamilyStrokes - perverse stiefmutter und stiefvater entlarven das sexgeheimnis ihrer frechen stieftochter Jessae Rosae | xHamster", + "file_name": "FamilyStrokes - perverse stiefmutter und stiefvater entlarven das sexgeheimnis ihrer frechen stieftochter Jessae Rosae [xhNjs2X].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/e5bfbada-d059-4488-b043-98f3478a8a70.mp4" + }, + { + "id": "e5c4b753-660e-4c61-9ba1-90bb9fe36e71", + "created_date": "2024-07-25 07:29:45.267345", + "last_modified_date": "2024-07-25 07:29:45.267345", + "version": 0, + "url": "https://ge.xhamster.com/videos/freeuse-new-years-eve-sex-party-teamskeet-xhUnr5O", + "review": 0, + "should_download": 0, + "title": "FreeUse silvester-sex-party - teamSkeet | xHamster", + "file_name": "FreeUse silvester-sex-party - teamSkeet [xhUnr5O].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e5c4b753-660e-4c61-9ba1-90bb9fe36e71.mp4" + }, + { + "id": "e616807c-b138-4341-83ff-a9cb0b320f3f", + "created_date": "2024-07-25 07:29:47.051272", + "last_modified_date": "2024-07-25 07:29:47.051272", + "version": 0, + "url": "https://ge.xhamster.com/videos/outdoor-family-therapy-groupsex-orgy-13987544", + "review": 0, + "should_download": 0, + "title": "Outdoor-Familientherapie-Gruppensex-Orgie | xHamster", + "file_name": "Outdoor-Familientherapie-Gruppensex-Orgie [13987544].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e616807c-b138-4341-83ff-a9cb0b320f3f.mp4" + }, + { + "id": "e6519025-f618-4e65-92f1-1302cb2a5c98", + "created_date": "2024-07-25 07:29:46.710487", + "last_modified_date": "2024-07-25 07:29:46.710487", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepbrother-entertain-me-with-outdoor-anal-sex-by-the-pool-xhtuzIs", + "review": 0, + "should_download": 0, + "title": "Stiefbruer, unterhalten sie mich mit analsex im freien am pool. | xHamster", + "file_name": "Stiefbruer, unterhalten sie mich mit analsex im freien am pool. [xhtuzIs].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e6519025-f618-4e65-92f1-1302cb2a5c98.mp4" + }, + { + "id": "e664a34c-280e-4a30-949e-583b990e7cf7", + "created_date": "2024-07-25 07:29:45.378839", + "last_modified_date": "2024-07-25 07:29:45.378839", + "version": 0, + "url": "https://ge.xhamster.com/videos/redhead-stepsister-gives-an-amazing-blowjob-in-pov-14091400", + "review": 0, + "should_download": 0, + "title": "Rothaarige Stiefschwester gibt einen erstaunlichen Blowjob in POV | xHamster", + "file_name": "Rothaarige Stiefschwester gibt einen erstaunlichen Blowjob in POV [14091400].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e664a34c-280e-4a30-949e-583b990e7cf7.mp4" + }, + { + "id": "e6cd0cd7-6f15-4a91-be5b-63d9033c4e60", + "created_date": "2024-07-25 07:29:46.844974", + "last_modified_date": "2024-07-25 07:29:46.844974", + "version": 0, + "url": "https://ge.xhamster.com/videos/junge-schlampen-auf-der-suche-nach-orgasmus-full-movie-xhu6G2x", + "review": 0, + "should_download": 0, + "title": "Junge Schlampen Auf Der Suche Nach Orgasmus Full Movie | xHamster", + "file_name": "Junge Schlampen auf der Suche nach Orgasmus (Full Movie) [xhu6G2x].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e6cd0cd7-6f15-4a91-be5b-63d9033c4e60.mp4" + }, + { + "id": "e6cd3333-c84b-4594-a744-3fa6eb676810", + "created_date": "2024-07-25 07:29:47.393574", + "last_modified_date": "2024-07-25 07:29:47.393574", + "version": 0, + "url": "https://ge.xhamster.com/videos/wild-weekend-14627578", + "review": 0, + "should_download": 0, + "title": "Wildes Wochenende | xHamster", + "file_name": "Wildes Wochenende [14627578].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e6cd3333-c84b-4594-a744-3fa6eb676810.mp4" + }, + { + "id": "e6d72b5f-eb2d-4446-a0c5-be96c49b685c", + "created_date": "2024-07-25 07:29:44.868766", + "last_modified_date": "2024-07-25 07:29:44.868766", + "version": 0, + "url": "https://ge.xhamster.com/videos/husband-share-wife-12126645", + "review": 0, + "should_download": 0, + "title": "Ehemann teilt Ehefrau | xHamster", + "file_name": "Ehemann teilt Ehefrau [12126645].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e6d72b5f-eb2d-4446-a0c5-be96c49b685c.mp4" + }, + { + "id": "e6dfc9c5-de92-4930-8e96-327cb1f51b31", + "created_date": "2024-07-25 07:29:47.217276", + "last_modified_date": "2024-07-25 07:29:47.217276", + "version": 0, + "url": "https://ge.xhamster.com/videos/meine-stiefsohnreaktion-wenn-ich-kein-hoeschen-trage566-xhokghx", + "review": 0, + "should_download": 0, + "title": "Meine Stiefsohnreaktion Wenn Ich Kein Hoeschen Trage566 | xHamster", + "file_name": "Meine Stiefsohnreaktion, wenn ich kein Hoeschen trage566 [xhokghx].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e6dfc9c5-de92-4930-8e96-327cb1f51b31.mp4" + }, + { + "id": "e6f18f85-6ce2-4c20-9668-a58e88eab91b", + "created_date": "2024-07-25 07:29:45.329462", + "last_modified_date": "2024-07-25 07:29:45.329462", + "version": 0, + "url": "https://ge.xhamster.com/videos/it-s-so-big-new-potential-roommate-catches-roomy-masturbating-to-porn-xhvkgSg", + "review": 0, + "should_download": 0, + "title": "Es ist so gro\u00df! neuer potenzieller Mitbewohner erwischt ger\u00e4umiges Masturbieren zum Porno | xHamster", + "file_name": "Es ist so gro\u00df! neuer potenzieller Mitbewohner erwischt ger\u00e4umiges Masturbieren zum Porno [xhvkgSg].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/e6f18f85-6ce2-4c20-9668-a58e88eab91b.mp4" + }, + { + "id": "e6fe3c76-6f00-4325-bd13-84bbfaf04242", + "created_date": "2024-07-25 07:29:46.344442", + "last_modified_date": "2024-07-25 07:29:46.344442", + "version": 0, + "url": "https://ge.xhamster.com/videos/class-orgy-7012854", + "review": 0, + "should_download": 0, + "title": "Klassenorgie. | xHamster", + "file_name": "Klassenorgie. [7012854].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e6fe3c76-6f00-4325-bd13-84bbfaf04242.mp4" + }, + { + "id": "e7940f48-a47e-4be7-9534-b374138bde94", + "created_date": "2024-08-28 23:21:54.372638", + "last_modified_date": "2024-08-28 23:21:54.372638", + "version": 0, + "url": "https://ge.xhamster.com/videos/agent-aika-4-ova-anime-1997-xhcYkOW", + "review": 0, + "should_download": 0, + "title": "Agent Aika 4 Ova Anime 1997, Free Comic Porn e0 | xHamster", + "file_name": "Agent Aika #4 OVA anime (1997) [xhcYkOW].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/e7940f48-a47e-4be7-9534-b374138bde94.mp4" + }, + { + "id": "e7af56ac-feb4-4a62-b667-192eba01e3c3", + "created_date": "2024-07-25 07:29:48.075053", + "last_modified_date": "2024-07-25 07:29:48.075053", + "version": 0, + "url": "https://ge.xhamster.com/videos/sexual-heights-1979-12884357", + "review": 0, + "should_download": 0, + "title": "Sexuelle H\u00f6hen (1979) | xHamster", + "file_name": "Sexuelle H\u00f6hen (1979) [12884357].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e7af56ac-feb4-4a62-b667-192eba01e3c3.mp4" + }, + { + "id": "e7d82f2f-4019-4a88-83ec-88394ecf96a6", + "created_date": "2024-10-21 15:08:43.558135", + "last_modified_date": "2024-10-21 16:33:28.962000", + "version": 1, + "url": "https://ge.xhamster.com/videos/drncm-classic-dp-g23-xhkz13C", + "review": 0, + "should_download": 0, + "title": "Drncm Klassiker, Doppelpenetration G23 | xHamster", + "file_name": "Drncm Klassiker, Doppelpenetration G23 [xhkz13C].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/e7d82f2f-4019-4a88-83ec-88394ecf96a6.mp4" + }, + { + "id": "e8087c24-3923-437d-a446-ab8ce23b8579", + "created_date": "2024-07-25 07:29:48.129263", + "last_modified_date": "2024-07-25 07:29:48.129263", + "version": 0, + "url": "https://ge.xhamster.com/videos/durchgefickt-und-abgesaugt-6790032", + "review": 0, + "should_download": 0, + "title": "Durchgefickt Und Abgesaugt, Free Anal Porn 4b | xHamster", + "file_name": "Durchgefickt Und Abgesaugt [6790032].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e8087c24-3923-437d-a446-ab8ce23b8579.mp4" + }, + { + "id": "e822d1b7-c689-4584-acb1-5217ead4ccf7", + "created_date": "2025-01-16 19:59:35.850005", + "last_modified_date": "2025-01-16 19:59:35.850011", + "version": 0, + "url": "https://ge.xhamster.com/videos/two-guys-and-two-girls-play-highest-card-wins-4155434", + "review": 0, + "should_download": 0, + "title": "Zwei Typen und zwei M\u00e4dchen spielen mit den h\u00f6chsten Kartensiegen | xHamster", + "file_name": "Zwei Typen und zwei M\u00e4dchen spielen mit den h\u00f6chsten Kartensiegen [4155434].mp4", + "path": null, + "cloud_link": "/data/media/e822d1b7-c689-4584-acb1-5217ead4ccf7.mp4" + }, + { + "id": "e883e73a-696d-41cf-b1ca-9a3be09723e1", + "created_date": "2024-07-25 07:29:45.718880", + "last_modified_date": "2024-07-25 07:29:45.718880", + "version": 0, + "url": "https://ge.xhamster.com/videos/deutsche-geilheit-verliebter-frauen-full-movie-xhxmN35", + "review": 0, + "should_download": 0, + "title": "Deutsche Geilheit Verliebter Frauen Full Movie: Porn a1 | xHamster", + "file_name": "Deutsche Geilheit verliebter Frauen (Full Movie) [xhxmN35].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e883e73a-696d-41cf-b1ca-9a3be09723e1.mp4" + }, + { + "id": "e8878e4f-a510-4f50-b8cc-7be06e1b5f5b", + "created_date": "2024-07-25 07:29:47.390141", + "last_modified_date": "2024-07-25 07:29:47.390141", + "version": 0, + "url": "https://ge.xhamster.com/videos/horny-step-siblings-love-masturbating-together-they-get-caught-all-the-time-pervtherapy-xhx6DaxR", + "review": 0, + "should_download": 0, + "title": "Geile stiefgeschwister lieben es, zusammen zu masturbieren und sie werden die ganze zeit erwischt - pervtherapie | xHamster", + "file_name": "Geile stiefgeschwister lieben es, zusammen zu masturbieren und sie werden die ganze zeit erwischt - pervtherapie [xhx6DaxR].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e8878e4f-a510-4f50-b8cc-7be06e1b5f5b.mp4" + }, + { + "id": "e889e5dd-53ca-4669-b60e-bab92f8f448b", + "created_date": "2024-07-25 07:29:47.333679", + "last_modified_date": "2024-07-25 07:29:47.333679", + "version": 0, + "url": "https://ge.xhamster.com/videos/for-the-love-of-pleasure-1979-xhWwBEx", + "review": 0, + "should_download": 0, + "title": "F\u00fcr die Liebe zum Vergn\u00fcgen (1979) | xHamster", + "file_name": "F\u00fcr die Liebe zum Vergn\u00fcgen (1979) [xhWwBEx].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e889e5dd-53ca-4669-b60e-bab92f8f448b.mp4" + }, + { + "id": "e8aa5da4-637e-4fd4-831b-e0b12856f4e0", + "created_date": "2024-07-25 07:29:46.990633", + "last_modified_date": "2024-07-25 07:29:46.990633", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-dirty-stepsister-plays-with-her-wet-pussy-xho05mY", + "review": 0, + "should_download": 0, + "title": "Meine schmutzige stiefschwester spielt mit ihrer nassen muschi | xHamster", + "file_name": "Meine schmutzige stiefschwester spielt mit ihrer nassen muschi [xho05mY].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e8aa5da4-637e-4fd4-831b-e0b12856f4e0.mp4" + }, + { + "id": "e9a0c168-28d6-4b01-96c7-9907bf7e4c36", + "created_date": "2024-07-25 07:29:46.783683", + "last_modified_date": "2024-07-25 07:29:46.783683", + "version": 0, + "url": "https://ge.xhamster.com/videos/redhead-step-sister-s7-e4-xhwmhPN", + "review": 0, + "should_download": 0, + "title": "Rothaarige stiefschweige - s7: e4 | xHamster", + "file_name": "Rothaarige stiefschweige - s7\uff1a e4 [xhwmhPN].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/e9a0c168-28d6-4b01-96c7-9907bf7e4c36.mp4" + }, + { + "id": "e9aaf691-d633-476d-b1f7-0faafe2df694", + "created_date": "2024-10-21 15:08:43.548645", + "last_modified_date": "2024-10-21 16:33:31.842000", + "version": 1, + "url": "https://ge.xhamster.com/videos/sex-in-two-holes-2850403", + "review": 0, + "should_download": 0, + "title": "Sex in zwei L\u00f6chern | xHamster", + "file_name": "Sex in zwei L\u00f6chern [2850403].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/e9aaf691-d633-476d-b1f7-0faafe2df694.mp4" + }, + { + "id": "e9dd2738-805f-4f83-bb7e-4b49b502459e", + "created_date": "2024-10-07 20:47:56.414103", + "last_modified_date": "2024-10-21 16:33:34.913000", + "version": 1, + "url": "https://ge.xhamster.com/videos/short-hair-housewife-doublejob-3416515", + "review": 0, + "should_download": 0, + "title": "Kurze Haare, Hausfrau, Doublejob | xHamster", + "file_name": "Kurze Haare, Hausfrau, Doublejob [3416515].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/e9dd2738-805f-4f83-bb7e-4b49b502459e.mp4" + }, + { + "id": "ead8d58d-1ac0-4c56-8f0e-8ad74b44c403", + "created_date": "2025-01-16 19:59:50.915391", + "last_modified_date": "2025-01-16 19:59:50.915398", + "version": 0, + "url": "https://ge.xhamster.com/videos/sexy-family-xh5FAXZ", + "review": 0, + "should_download": 0, + "title": "Sexy Familie | xHamster", + "file_name": "Sexy Familie [xh5FAXZ].mp4", + "path": null, + "cloud_link": "/data/media/ead8d58d-1ac0-4c56-8f0e-8ad74b44c403.mp4" + }, + { + "id": "eb0d4866-db0f-4a18-a91f-d413d725a7fa", + "created_date": "2024-07-25 07:29:47.494956", + "last_modified_date": "2024-07-25 07:29:47.494956", + "version": 0, + "url": "https://ge.xhamster.com/videos/indecency-1998-special-edition-xhg58Sz", + "review": 0, + "should_download": 0, + "title": "Indecency (1998) Sonderausgabe | xHamster", + "file_name": "Indecency (1998) Sonderausgabe [xhg58Sz].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/eb0d4866-db0f-4a18-a91f-d413d725a7fa.mp4" + }, + { + "id": "eb13a059-a705-4607-a858-12da1da59da0", + "created_date": "2024-07-25 07:29:45.759972", + "last_modified_date": "2024-07-25 07:29:45.759972", + "version": 0, + "url": "https://ge.xhamster.com/videos/mein-verfickter-urlaub-full-movie-xhETPMs", + "review": 0, + "should_download": 0, + "title": "Mein Verfickter Urlaub Full Movie, Free Porn 57 | xHamster", + "file_name": "Mein verfickter Urlaub (Full Movie) [xhETPMs].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/eb13a059-a705-4607-a858-12da1da59da0.mp4" + }, + { + "id": "eb1c5feb-15e5-4b6c-9e1a-ab2c9f9a327d", + "created_date": "2024-07-25 07:29:47.529304", + "last_modified_date": "2024-07-25 07:29:47.529304", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-step-sis-april-fools-prank-makes-me-cum-inside-her-s3-e4-11471257", + "review": 0, + "should_download": 0, + "title": "Meine Stiefschwester April Dummkopf Streich, bringt mich in ihr s3: e4 kommen | xHamster", + "file_name": "Meine Stiefschwester April Dummkopf Streich, bringt mich in ihr s3\uff1a e4 kommen [11471257].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/eb1c5feb-15e5-4b6c-9e1a-ab2c9f9a327d.mp4" + }, + { + "id": "eb88948e-f308-4640-89a0-cb5e12bc6706", + "created_date": "2024-07-25 07:29:44.787627", + "last_modified_date": "2024-07-25 07:29:44.787627", + "version": 0, + "url": "https://ge.xhamster.com/videos/horny-stepdaughter-hides-under-the-blanket-to-be-fucked-by-stepfathers-cock-and-make-a-threesome-with-stepmom-charlie-forde-xhc4G6J", + "review": 0, + "should_download": 0, + "title": "Horny Stepdaughter Hides Under the Blanket to be Fucked by Stepfather's Cock and Make a Threesome with Stepmom Charlie Forde | xHamster", + "file_name": "Horny stepdaughter hides under the blanket to be fucked by stepfather's cock and make a threesome with stepmom Charlie Forde. [xhc4G6J].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/eb88948e-f308-4640-89a0-cb5e12bc6706.mp4" + }, + { + "id": "ebc2aac1-755b-4536-9542-9a3acb00130b", + "created_date": "2024-07-25 07:29:46.149726", + "last_modified_date": "2024-07-25 07:29:46.149726", + "version": 0, + "url": "https://ge.xhamster.com/videos/i-like-to-watch-1982-xhFL5ZA", + "review": 0, + "should_download": 0, + "title": "Ich schaue gerne zu (1982) | xHamster", + "file_name": "Ich schaue gerne zu (1982) [xhFL5ZA].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ebc2aac1-755b-4536-9542-9a3acb00130b.mp4" + }, + { + "id": "ebc5033a-39eb-423f-ac2a-9fab8f287390", + "created_date": "2024-07-25 07:29:45.263590", + "last_modified_date": "2024-07-25 07:29:45.263590", + "version": 0, + "url": "https://ge.xhamster.com/videos/cousin-betty-1972-upscaled-to-4k-xhkLDji", + "review": 0, + "should_download": 0, + "title": "Cousin Betty (1972), auf 4k hochskaliert | xHamster", + "file_name": "Cousin Betty (1972), auf 4k hochskaliert [xhkLDji].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/ebc5033a-39eb-423f-ac2a-9fab8f287390.mp4" + }, + { + "id": "ec41225d-a3e7-4b6a-bc06-5b74eae33a44", + "created_date": "2024-07-25 07:29:45.704473", + "last_modified_date": "2024-07-25 07:29:45.704473", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-friends-hot-mom-is-ava-addams-xhzWc4I", + "review": 0, + "should_download": 0, + "title": "Die hei\u00dfe mutter meines freundes ist Ava addams | xHamster", + "file_name": "Die hei\u00dfe mutter meines freundes ist Ava addams [xhzWc4I].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/ec41225d-a3e7-4b6a-bc06-5b74eae33a44.mp4" + }, + { + "id": "ec459efe-f3df-4b7d-86cf-92a1265e2d8d", + "created_date": "2024-07-25 07:29:47.379520", + "last_modified_date": "2024-07-25 07:29:47.379520", + "version": 0, + "url": "https://ge.xhamster.com/videos/german-vintage-compilation-14508029", + "review": 0, + "should_download": 0, + "title": "Deutsche Retro-Zusammenstellung | xHamster", + "file_name": "Deutsche Retro-Zusammenstellung [14508029].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ec459efe-f3df-4b7d-86cf-92a1265e2d8d.mp4" + }, + { + "id": "ec495eea-2d88-4430-b9f8-47cda588787d", + "created_date": "2024-07-25 07:29:47.100655", + "last_modified_date": "2024-07-25 07:29:47.100655", + "version": 0, + "url": "https://ge.xhamster.com/videos/anal-3some-in-the-office-8086339", + "review": 0, + "should_download": 0, + "title": "Analer Dreier im B\u00fcro | xHamster", + "file_name": "Analer Dreier im B\u00fcro [8086339].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ec495eea-2d88-4430-b9f8-47cda588787d.mp4" + }, + { + "id": "ec7bd3fd-ccd5-48c5-a3dd-3beaf05f8fdb", + "created_date": "2024-09-24 08:11:39.000025", + "last_modified_date": "2024-10-21 16:33:40.600000", + "version": 1, + "url": "https://ge.xhamster.com/videos/virgin-stepson-begs-his-stepmom-to-show-him-how-to-fuck-xh6Wznu", + "review": 0, + "should_download": 0, + "title": "Virgin Stiefsohn fleht seine Stiefmutter an, ihm zu zeigen, wie man fickt | xHamster", + "file_name": "Virgin Stiefsohn fleht seine Stiefmutter an, ihm zu zeigen, wie man fickt [xh6Wznu].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/ec7bd3fd-ccd5-48c5-a3dd-3beaf05f8fdb.mp4" + }, + { + "id": "eca594b8-2982-4a41-b326-b55c0ab2bac5", + "created_date": "2024-07-25 07:29:46.445788", + "last_modified_date": "2024-07-25 07:29:46.445788", + "version": 0, + "url": "https://ge.xhamster.com/videos/jojo-kiss-caught-on-her-boyfriend-and-a-masseuse-9253178", + "review": 0, + "should_download": 0, + "title": "Jojo Kiss k\u00fcsste ihren Freund und eine Masseuse | xHamster", + "file_name": "Jojo Kiss k\u00fcsste ihren Freund und eine Masseuse [9253178].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/eca594b8-2982-4a41-b326-b55c0ab2bac5.mp4" + }, + { + "id": "ecace882-c3c0-4565-8268-15acadb073cf", + "created_date": "2024-07-25 07:29:47.438861", + "last_modified_date": "2024-07-25 07:29:47.438861", + "version": 0, + "url": "https://ge.xhamster.com/videos/naive-and-slutty-office-girl-fucking-the-wrong-cock-xhtd25C", + "review": 0, + "should_download": 0, + "title": "Naives und versautes B\u00fcrom\u00e4dchen fickt den falschen Schwanz | xHamster", + "file_name": "Naives und versautes B\u00fcrom\u00e4dchen fickt den falschen Schwanz [xhtd25C].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ecace882-c3c0-4565-8268-15acadb073cf.mp4" + }, + { + "id": "ecca935f-4bdd-4946-ad4c-695860585c9b", + "created_date": "2024-07-25 07:29:45.249005", + "last_modified_date": "2024-07-25 07:29:45.249005", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepmom-says-youre-having-sex-with-the-sponges-xhoKcsY", + "review": 0, + "should_download": 0, + "title": "Stiefmutter sagt, du hast Sex mit den Schw\u00e4mmen ?! | xHamster", + "file_name": "Stiefmutter sagt, du hast Sex mit den Schw\u00e4mmen \uff1f! [xhoKcsY].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ecca935f-4bdd-4946-ad4c-695860585c9b.mp4" + }, + { + "id": "eceb3620-68fd-4a3c-9928-70c169ce4b3f", + "created_date": "2024-07-25 07:29:44.745680", + "last_modified_date": "2024-07-25 07:29:44.745680", + "version": 0, + "url": "https://ge.xhamster.com/videos/flirt-or-fuck-s41-e13-xhajGiY", + "review": 0, + "should_download": 0, + "title": "Flirten oder Ficken - S41:E13 | xHamster", + "file_name": "Flirten oder Ficken - S41\uff1aE13 [xhajGiY].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/eceb3620-68fd-4a3c-9928-70c169ce4b3f.mp4" + }, + { + "id": "ed22a066-5070-46ab-9e07-c43ebef8d44b", + "created_date": "2024-12-29 23:53:27.960430", + "last_modified_date": "2024-12-29 23:53:27.960430", + "version": 0, + "url": "https://ge.xhamster.com/videos/emmanuel-480064", + "review": 0, + "should_download": 0, + "title": "Emmanuel | xHamster", + "file_name": "Emmanuel [480064].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/ed22a066-5070-46ab-9e07-c43ebef8d44b.mp4" + }, + { + "id": "ed236077-0b3d-4165-a157-0a215ca85663", + "created_date": "2024-12-29 23:53:27.925332", + "last_modified_date": "2024-12-29 23:53:27.925332", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-mischiefs-part-1-step-mom-seduces-her-step-daughters-huge-dick-boyfriend-xhNTlTd", + "review": 0, + "should_download": 0, + "title": "Familien-unfug (teil 1): Stiefmutter verf\u00fchrt den freund ihrer stieftochter mit riesigem schwanz | xHamster", + "file_name": "Familien-unfug (teil 1)\uff1a Stiefmutter verf\u00fchrt den freund ihrer stieftochter mit riesigem schwanz [xhNTlTd].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/ed236077-0b3d-4165-a157-0a215ca85663.mp4" + }, + { + "id": "ed3df055-47bb-417b-988f-82d2e7c51f4a", + "created_date": "2024-12-29 23:53:27.937612", + "last_modified_date": "2024-12-29 23:53:27.937612", + "version": 0, + "url": "https://ge.xhamster.com/videos/family-road-trip-xhvC9bX", + "review": 0, + "should_download": 0, + "title": "Familien-Roadtrip | xHamster", + "file_name": "Familien-Roadtrip [xhvC9bX].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/ed3df055-47bb-417b-988f-82d2e7c51f4a.mp4" + }, + { + "id": "ed5aa02b-6635-4f1b-b972-81fc8f6bcc4b", + "created_date": "2024-07-25 07:29:46.637832", + "last_modified_date": "2024-07-25 07:29:46.637832", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-village-swinging-exploration-2846875", + "review": 0, + "should_download": 0, + "title": "Das Dorf schwingt erforscht | xHamster", + "file_name": "Das Dorf schwingt erforscht [2846875].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ed5aa02b-6635-4f1b-b972-81fc8f6bcc4b.mp4" + }, + { + "id": "ed882c47-0b1f-443d-bce7-fe36fac2682f", + "created_date": "2024-07-25 07:29:48.165475", + "last_modified_date": "2024-07-25 07:29:48.165475", + "version": 0, + "url": "https://ge.xhamster.com/videos/jugendlicher-babysitter-macht-verschwitzten-gruppensex-xhscfbV", + "review": 0, + "should_download": 0, + "title": "Jugendlicher Babysitter Macht Verschwitzten Gruppensex | xHamster", + "file_name": "Jugendlicher Babysitter macht verschwitzten Gruppensex [xhscfbV].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ed882c47-0b1f-443d-bce7-fe36fac2682f.mp4" + }, + { + "id": "ed972468-a0ec-4601-9efd-bcebad854786", + "created_date": "2024-07-25 07:29:46.348627", + "last_modified_date": "2024-07-25 07:29:46.348627", + "version": 0, + "url": "https://ge.xhamster.com/videos/a-beautiful-german-girl-loves-pleasing-a-cock-outdoors-xhwCQSh", + "review": 0, + "should_download": 0, + "title": "Ein sch\u00f6nes deutsches M\u00e4dchen liebt es, einen Schwanz im Freien zu befriedigen | xHamster", + "file_name": "Ein sch\u00f6nes deutsches M\u00e4dchen liebt es, einen Schwanz im Freien zu befriedigen [xhwCQSh].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ed972468-a0ec-4601-9efd-bcebad854786.mp4" + }, + { + "id": "edc29973-ef60-44e1-bd2d-5b1dc148518d", + "created_date": "2024-07-25 07:29:45.993104", + "last_modified_date": "2024-07-25 07:29:45.993104", + "version": 0, + "url": "https://ge.xhamster.com/videos/wife-shared-at-party-10366744", + "review": 0, + "should_download": 0, + "title": "Ehefrau auf der Party geteilt | xHamster", + "file_name": "Ehefrau auf der Party geteilt [10366744].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/edc29973-ef60-44e1-bd2d-5b1dc148518d.mp4" + }, + { + "id": "edc50f6d-1a45-4db3-96ad-e1108d910e3f", + "created_date": "2024-07-25 07:29:45.170871", + "last_modified_date": "2024-07-25 07:29:45.170871", + "version": 0, + "url": "https://ge.xhamster.com/videos/sommertraume-junger-madchen-nr-3-full-movie-xhcIHl6", + "review": 0, + "should_download": 0, + "title": "Sommertr\u00e4ume junger m\u00e4dchen # 3 (kompletter film) | xHamster", + "file_name": "Sommertr\u00e4ume junger m\u00e4dchen # 3 (kompletter film) [xhcIHl6].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/edc50f6d-1a45-4db3-96ad-e1108d910e3f.mp4" + }, + { + "id": "edf758b6-05a4-43d5-996b-dce819b08fe4", + "created_date": "2024-12-29 23:53:27.895919", + "last_modified_date": "2024-12-29 23:53:27.895919", + "version": 0, + "url": "https://ge.xhamster.com/videos/sugar-lyn-beard-palm-swings-2017-9921879", + "review": 0, + "should_download": 0, + "title": "Sugar Lyn Bart - Palm Swings (2017) | xHamster", + "file_name": "Sugar Lyn Bart - Palm Swings (2017) [9921879].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/edf758b6-05a4-43d5-996b-dce819b08fe4.mp4" + }, + { + "id": "ee461aa7-9b82-45e9-9796-bc441915ae85", + "created_date": "2024-07-25 07:29:45.192387", + "last_modified_date": "2024-07-25 07:29:45.192387", + "version": 0, + "url": "https://ge.xhamster.com/videos/babysitter-comes-over-early-and-spied-on-wife-sucking-off-husband-xhPuDCm", + "review": 0, + "should_download": 0, + "title": "Babysitter kommt fr\u00fch r\u00fcber und bespitzelt Ehefrau, die Ehemann lutscht | xHamster", + "file_name": "Babysitter kommt fr\u00fch r\u00fcber und bespitzelt Ehefrau, die Ehemann lutscht [xhPuDCm].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ee461aa7-9b82-45e9-9796-bc441915ae85.mp4" + }, + { + "id": "ee4a6b31-6ce4-4e31-a717-0a8d0a06a5f7", + "created_date": "2024-07-25 07:29:46.967665", + "last_modified_date": "2024-07-25 07:29:46.967665", + "version": 0, + "url": "https://ge.xhamster.com/videos/bathtub-threesome-with-the-hot-stepmom-monique-alexander-6815912", + "review": 0, + "should_download": 0, + "title": "Badewannen-Dreier mit der hei\u00dfen Stiefmutter Monique Alexander | xHamster", + "file_name": "Badewannen-Dreier mit der hei\u00dfen Stiefmutter Monique Alexander [6815912].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ee4a6b31-6ce4-4e31-a717-0a8d0a06a5f7.mp4" + }, + { + "id": "ee843861-3e4c-4ceb-8550-db022f5e2fc7", + "created_date": "2024-07-25 07:29:46.598576", + "last_modified_date": "2024-07-25 07:29:46.598576", + "version": 0, + "url": "https://ge.xhamster.com/videos/a-sibel-kekilli-classic-vol-4-13437069", + "review": 0, + "should_download": 0, + "title": "Hotel Fickmichgut | xHamster", + "file_name": "Hotel Fickmichgut [13437069].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ee843861-3e4c-4ceb-8550-db022f5e2fc7.mp4" + }, + { + "id": "ee8f2b8a-79fc-49b6-a35b-b618a01c983c", + "created_date": "2024-07-25 07:29:44.890163", + "last_modified_date": "2024-07-25 07:29:44.890163", + "version": 0, + "url": "https://ge.xhamster.com/videos/lost-bet-now-she-had-to-fuck-3-guys-at-the-same-time-xhOkSrk", + "review": 0, + "should_download": 0, + "title": "Lost Bet, jetzt musste sie 3 Typen gleichzeitig ficken | xHamster", + "file_name": "Lost Bet, jetzt musste sie 3 Typen gleichzeitig ficken [xhOkSrk].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/ee8f2b8a-79fc-49b6-a35b-b618a01c983c.mp4" + }, + { + "id": "ee92b520-3dad-4703-a2f3-8f8f1f6fe4d1", + "created_date": "2024-07-25 07:29:46.741072", + "last_modified_date": "2024-07-25 07:29:46.741072", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-neighbor-catches-me-masturbating-in-the-pool-and-invites-me-to-fuck-andrea-pardo-xhecMDu", + "review": 0, + "should_download": 0, + "title": "Meine Nachbarin erwischt mich beim Masturbieren im Pool und l\u00e4dt mich zum Ficken ein - Andrea Pardo | xHamster", + "file_name": "Meine Nachbarin erwischt mich beim Masturbieren im Pool und l\u00e4dt mich zum Ficken ein - Andrea Pardo [xhecMDu].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ee92b520-3dad-4703-a2f3-8f8f1f6fe4d1.mp4" + }, + { + "id": "ee9b512a-348e-4301-b9cd-312ed37c69d4", + "created_date": "2024-07-25 07:29:46.285885", + "last_modified_date": "2024-07-25 07:29:46.285885", + "version": 0, + "url": "https://ge.xhamster.com/videos/mature-wife-catches-husband-jerking-off-and-doesnt-mind-xho8Vlh", + "review": 0, + "should_download": 0, + "title": "Reife ehefrau erwischt ehemann beim wichsen und hat nichts dagegen | xHamster", + "file_name": "Reife ehefrau erwischt ehemann beim wichsen und hat nichts dagegen [xho8Vlh].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ee9b512a-348e-4301-b9cd-312ed37c69d4.mp4" + }, + { + "id": "eea33597-c1c4-4ddb-91c4-52ba2d9dba2f", + "created_date": "2024-07-25 07:29:46.424414", + "last_modified_date": "2024-07-25 07:29:46.424414", + "version": 0, + "url": "https://ge.xhamster.com/videos/that-was-really-hot-i-was-wondering-if-you-could-fuck-me-xhuyqu3", + "review": 0, + "should_download": 0, + "title": "Das war wirklich hei\u00df, ich habe mich gefragt, ob du mich ficken kannst | xHamster", + "file_name": "Das war wirklich hei\u00df, ich habe mich gefragt, ob du mich ficken kannst [xhuyqu3].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/eea33597-c1c4-4ddb-91c4-52ba2d9dba2f.mp4" + }, + { + "id": "eef22f5f-f3c1-4971-b63f-759316926902", + "created_date": "2024-07-25 07:29:47.517861", + "last_modified_date": "2025-01-03 11:56:51.864000", + "version": 1, + "url": "https://ge.xhamster.com/videos/insane-group-fucking-and-pissing-by-summersinners-xhG951b", + "review": 0, + "should_download": 0, + "title": "Wahnsinnige gruppe Ficken und Pissen von SummerSinners | xHamster", + "file_name": "Wahnsinnige gruppe Ficken und Pissen von SummerSinners [xhG951b].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/eef22f5f-f3c1-4971-b63f-759316926902.mp4" + }, + { + "id": "ef004f13-26bf-4729-8de6-66a30f0c2259", + "created_date": "2024-07-25 07:29:45.883816", + "last_modified_date": "2024-07-25 07:29:45.883816", + "version": 0, + "url": "https://ge.xhamster.com/videos/its-definitely-bigger-than-mypervyfamily-xhkScYv", + "review": 0, + "should_download": 0, + "title": "\"Es ist auf jeden Fall gr\u00f6\u00dfer als...\" - Mypervyfamily | xHamster", + "file_name": "\uff02Es ist auf jeden Fall gr\u00f6\u00dfer als...\uff02 - Mypervyfamily [xhkScYv].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ef004f13-26bf-4729-8de6-66a30f0c2259.mp4" + }, + { + "id": "ef83d783-8f0f-4d0b-ad50-68d98e3ec0a8", + "created_date": "2024-07-25 07:29:47.899891", + "last_modified_date": "2024-07-25 07:29:47.899891", + "version": 0, + "url": "https://ge.xhamster.com/videos/two-lucky-dudes-breach-all-the-tight-hot-young-holes-on-this-perfect-blonde-xhFEnIh", + "review": 0, + "should_download": 0, + "title": "Zwei gl\u00fcckliche Typen durchbrechen alle engen hei\u00dfen jungen L\u00f6cher dieser perfekten Blondine | xHamster", + "file_name": "Zwei gl\u00fcckliche Typen durchbrechen alle engen hei\u00dfen jungen L\u00f6cher dieser perfekten Blondine [xhFEnIh].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ef83d783-8f0f-4d0b-ad50-68d98e3ec0a8.mp4" + }, + { + "id": "efa08b76-c334-48da-b7e6-b11a4e230d37", + "created_date": "2024-07-25 07:29:44.736600", + "last_modified_date": "2024-07-25 07:29:44.736600", + "version": 0, + "url": "https://ge.xhamster.com/videos/hot-stepmom-sharon-white-shares-boyfriend-s-cock-xhqk2HR", + "review": 0, + "should_download": 0, + "title": "Hei\u00dfe Stiefmutter Sharon White teilt den Schwanz ihres Freundes | xHamster", + "file_name": "Hei\u00dfe Stiefmutter Sharon White teilt den Schwanz ihres Freundes [xhqk2HR].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/efa08b76-c334-48da-b7e6-b11a4e230d37.mp4" + }, + { + "id": "f048051c-ecc8-475a-94bc-99dcbee3abb8", + "created_date": "2024-07-25 07:29:47.999304", + "last_modified_date": "2024-07-25 07:29:47.999304", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsis-catches-me-my-friends-sniffing-her-panties-14250952", + "review": 0, + "should_download": 0, + "title": "Stiefschwester erwischt mich & meine Freunde beim Schn\u00fcffeln an ihrem H\u00f6schen | xHamster", + "file_name": "Stiefschwester erwischt mich & meine Freunde beim Schn\u00fcffeln an ihrem H\u00f6schen [14250952].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f048051c-ecc8-475a-94bc-99dcbee3abb8.mp4" + }, + { + "id": "f082dc83-be35-43c2-b6a2-ba13b72baa94", + "created_date": "2024-07-25 07:29:45.139276", + "last_modified_date": "2024-07-25 07:29:45.139276", + "version": 0, + "url": "https://ge.xhamster.com/videos/donau-sex-full-german-movie-xhRzz9S", + "review": 0, + "should_download": 0, + "title": "Donau Sex, kompletter deutscher Film | xHamster", + "file_name": "Donau Sex, kompletter deutscher Film [xhRzz9S].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/f082dc83-be35-43c2-b6a2-ba13b72baa94.mp4" + }, + { + "id": "f0b35027-8b8c-4dec-a6d7-965448eae99d", + "created_date": "2024-07-25 07:29:46.769168", + "last_modified_date": "2024-07-25 07:29:46.769168", + "version": 0, + "url": "https://ge.xhamster.com/videos/sex-sex-and-fun-6753859", + "review": 0, + "should_download": 0, + "title": "Sex Sex Sex und Spa\u00df | xHamster", + "file_name": "Sex Sex Sex und Spa\u00df [6753859].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f0b35027-8b8c-4dec-a6d7-965448eae99d.mp4" + }, + { + "id": "f0bb4c23-6b43-477c-9afe-8017ee5828b8", + "created_date": "2024-07-25 07:29:45.321819", + "last_modified_date": "2024-07-25 07:29:45.321819", + "version": 0, + "url": "https://ge.xhamster.com/videos/sarah-and-friends-11-1994-german-full-dvd-rip-xhn7u6y", + "review": 0, + "should_download": 0, + "title": "Sarah und Freunde 11 (1994, deutsch, kompletter DVD-Rip) | xHamster", + "file_name": "Sarah und Freunde 11 (1994, deutsch, kompletter DVD-Rip) [xhn7u6y].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/f0bb4c23-6b43-477c-9afe-8017ee5828b8.mp4" + }, + { + "id": "f0d9b89c-7a08-43c3-805b-b6700f14667f", + "created_date": "2024-07-25 07:29:47.487827", + "last_modified_date": "2024-07-25 07:29:47.487827", + "version": 0, + "url": "https://ge.xhamster.com/videos/german-schwanzgeile-jungfrauen-1290630", + "review": 0, + "should_download": 0, + "title": "Deutsche schwanzgeile Jungfrauen | xHamster", + "file_name": "Deutsche schwanzgeile Jungfrauen [1290630].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f0d9b89c-7a08-43c3-805b-b6700f14667f.mp4" + }, + { + "id": "f0e2ad5e-46e2-490b-bdb1-f7366d6190b4", + "created_date": "2024-07-25 07:29:47.430626", + "last_modified_date": "2024-07-25 07:29:47.430626", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepbrother-stepsister-learn-to-get-along-family-therapy-xhsyAAg", + "review": 0, + "should_download": 0, + "title": "Stiefbruder und Stiefschwester lernen, miteinander auszukommen - Familientherapie | xHamster", + "file_name": "Stiefbruder und Stiefschwester lernen, miteinander auszukommen - Familientherapie [xhsyAAg].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f0e2ad5e-46e2-490b-bdb1-f7366d6190b4.mp4" + }, + { + "id": "f0f9a018-3e95-461c-aa52-3527e69f903d", + "created_date": "2024-07-25 07:29:47.813306", + "last_modified_date": "2024-07-25 07:29:47.813306", + "version": 0, + "url": "https://ge.xhamster.com/videos/lust-boat-1984-9976817", + "review": 0, + "should_download": 0, + "title": "Lust Boat (1984) | xHamster", + "file_name": "Lust Boat (1984) [9976817].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f0f9a018-3e95-461c-aa52-3527e69f903d.mp4" + }, + { + "id": "f103a45a-754d-4a73-8a5f-0335d1f71d22", + "created_date": "2024-07-25 07:29:47.563713", + "last_modified_date": "2024-07-25 07:29:47.563713", + "version": 0, + "url": "https://ge.xhamster.com/videos/die-wilden-lueste-meiner-schulfreundinnen-1984-6220701", + "review": 0, + "should_download": 0, + "title": "Die Wilden Lueste Meiner Schulfreundinnen 1984: Porn db | xHamster", + "file_name": "Die wilden Lueste meiner Schulfreundinnen (1984) [6220701].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f103a45a-754d-4a73-8a5f-0335d1f71d22.mp4" + }, + { + "id": "f11b051f-96a4-4b75-9e12-ab703e7c5c90", + "created_date": "2024-07-25 07:29:47.608383", + "last_modified_date": "2024-07-25 07:29:47.608383", + "version": 0, + "url": "https://ge.xhamster.com/videos/schulmadchen-report-2-1971-5765359", + "review": 0, + "should_download": 0, + "title": "Schulmadchen-Bericht 2 (1971) | xHamster", + "file_name": "Schulmadchen-Bericht 2 (1971) [5765359].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f11b051f-96a4-4b75-9e12-ab703e7c5c90.mp4" + }, + { + "id": "f11b504d-5742-4bc1-831f-15562ec4c451", + "created_date": "2024-07-25 07:29:44.733091", + "last_modified_date": "2024-07-25 07:29:44.733091", + "version": 0, + "url": "https://ge.xhamster.com/videos/horny-friends-fucking-by-the-pool-xhMYg9o", + "review": 0, + "should_download": 0, + "title": "Geile Freunde ficken am Pool | xHamster", + "file_name": "Geile Freunde ficken am Pool [xhMYg9o].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f11b504d-5742-4bc1-831f-15562ec4c451.mp4" + }, + { + "id": "f193b97e-ef9e-465e-842c-628b4911868c", + "created_date": "2024-07-25 07:29:47.832210", + "last_modified_date": "2024-07-25 07:29:47.832210", + "version": 0, + "url": "https://ge.xhamster.com/videos/double-squirt-vol-04-xhPqT4A", + "review": 0, + "should_download": 0, + "title": "Doppelter Squirt !!! - vol. # 04 | xHamster", + "file_name": "Doppelter Squirt !!! - vol. # 04 [xhPqT4A].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f193b97e-ef9e-465e-842c-628b4911868c.mp4" + }, + { + "id": "f2094653-7b0f-4a50-9cf1-f3625aefcd0b", + "created_date": "2024-07-25 07:29:45.505698", + "last_modified_date": "2024-07-25 07:29:45.505698", + "version": 0, + "url": "https://ge.xhamster.com/videos/fuck-at-the-cinema-watching-a-porn-movie-xhxOoSG", + "review": 0, + "should_download": 0, + "title": "Fick im kino, w\u00e4hrend du einen pornofilm gucke | xHamster", + "file_name": "Fick im kino, w\u00e4hrend du einen pornofilm gucke [xhxOoSG].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f2094653-7b0f-4a50-9cf1-f3625aefcd0b.mp4" + }, + { + "id": "f277e107-8428-4ad3-bdfc-0ee35c1581f6", + "created_date": "2024-12-29 23:53:27.890471", + "last_modified_date": "2024-12-29 23:53:27.890471", + "version": 0, + "url": "https://ge.xhamster.com/videos/young-teacher-lets-students-fuck-at-her-place-7509586", + "review": 0, + "should_download": 0, + "title": "Junge Lehrerin l\u00e4sst Sch\u00fcler bei ihr ficken | xHamster", + "file_name": "Junge Lehrerin l\u00e4sst Sch\u00fcler bei ihr ficken [7509586].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/f277e107-8428-4ad3-bdfc-0ee35c1581f6.mp4" + }, + { + "id": "f2a439d3-aeaf-44a9-aee2-89b950ed21c6", + "created_date": "2024-07-25 07:29:44.650780", + "last_modified_date": "2024-07-25 07:29:44.650780", + "version": 0, + "url": "https://ge.xhamster.com/videos/mom-daughter-and-step-son-have-hardcore-sex-xhfhVdk", + "review": 0, + "should_download": 0, + "title": "Mutter, Stieftochter und Stiefsohn haben Hardcore-Sex | xHamster", + "file_name": "Mutter, Stieftochter und Stiefsohn haben Hardcore-Sex [xhfhVdk].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f2a439d3-aeaf-44a9-aee2-89b950ed21c6.mp4" + }, + { + "id": "f2e6aa40-38c9-4d15-88a1-786a32dd4adc", + "created_date": "2024-07-25 07:29:46.532161", + "last_modified_date": "2024-07-25 07:29:46.532161", + "version": 0, + "url": "https://ge.xhamster.com/videos/private-anny-aurora-and-alexis-crystal-celebrate-with-an-orgy-8793899", + "review": 0, + "should_download": 0, + "title": "Anny Aurora und Alexis Crystal feiern mit einer Orgie | xHamster", + "file_name": "Anny Aurora und Alexis Crystal feiern mit einer Orgie [8793899].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f2e6aa40-38c9-4d15-88a1-786a32dd4adc.mp4" + }, + { + "id": "f31c48cf-5cc8-4ee5-b284-0092b73657c6", + "created_date": "2024-07-25 07:29:47.679675", + "last_modified_date": "2024-07-25 07:29:47.679675", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsis-says-i-dont-care-i-just-want-you-to-be-in-me-xhz4ErN", + "review": 0, + "should_download": 0, + "title": "Stiefschwester sagt - es ist mir egal, ich will nur, dass du in mir bist | xHamster", + "file_name": "Stiefschwester sagt - es ist mir egal, ich will nur, dass du in mir bist [xhz4ErN].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f31c48cf-5cc8-4ee5-b284-0092b73657c6.mp4" + }, + { + "id": "f3793acd-e656-4c80-af82-4016013a83e1", + "created_date": "2024-10-07 20:47:56.415185", + "last_modified_date": "2024-10-21 16:33:50.480000", + "version": 1, + "url": "https://ge.xhamster.com/videos/vintage-70s-danish-sex-mad-maids-german-dub-cc79-212210", + "review": 0, + "should_download": 0, + "title": "Retro 70er d\u00e4nisch - sex-verr\u00fcckte Zimmerm\u00e4dchen (deutsche Synchronisation) - cc79 | xHamster", + "file_name": "Retro 70er d\u00e4nisch - sex-verr\u00fcckte Zimmerm\u00e4dchen (deutsche Synchronisation) - cc79 [212210].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/f3793acd-e656-4c80-af82-4016013a83e1.mp4" + }, + { + "id": "f382aed3-d899-41c0-8cac-8eb11f1bcba9", + "created_date": "2024-08-28 23:21:54.352448", + "last_modified_date": "2024-08-28 23:21:54.352448", + "version": 0, + "url": "https://ge.xhamster.com/videos/ultimate-home-orgy-5-1-dag83-9038173", + "review": 0, + "should_download": 0, + "title": "Ultimative Heimorgie 5.1 - dag83 | xHamster", + "file_name": "Ultimative Heimorgie 5.1 - dag83 [9038173].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/f382aed3-d899-41c0-8cac-8eb11f1bcba9.mp4" + }, + { + "id": "f3a734d0-c7e8-4491-8a7f-fafc9596545b", + "created_date": "2024-07-25 07:29:44.579063", + "last_modified_date": "2024-07-25 07:29:44.579063", + "version": 0, + "url": "https://ge.xhamster.com/videos/remembering-the-hot-days-of-summer-xhTh0ec", + "review": 0, + "should_download": 0, + "title": "Sich an die hei\u00dfen Sommertage erinnern | xHamster", + "file_name": "Sich an die hei\u00dfen Sommertage erinnern [xhTh0ec].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f3a734d0-c7e8-4491-8a7f-fafc9596545b.mp4" + }, + { + "id": "f43b0f09-5761-416b-b72d-fcb3744a0187", + "created_date": "2024-07-25 07:29:47.835862", + "last_modified_date": "2024-07-25 07:29:47.835862", + "version": 0, + "url": "https://ge.xhamster.com/videos/group-of-slutty-teens-get-smashed-by-jmac-on-spring-break-6168186", + "review": 0, + "should_download": 0, + "title": "Eine Gruppe versauter Teenager wird im Spring Break von Jmac zertr\u00fcmmert | xHamster", + "file_name": "Eine Gruppe versauter Teenager wird im Spring Break von Jmac zertr\u00fcmmert [6168186].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f43b0f09-5761-416b-b72d-fcb3744a0187.mp4" + }, + { + "id": "f490aea8-3051-4eb6-bc66-5e7c9f83df12", + "created_date": "2024-07-25 07:29:46.787750", + "last_modified_date": "2024-07-25 07:29:46.787750", + "version": 0, + "url": "https://ge.xhamster.com/videos/nothing-can-stop-hottie-angel-youngs-and-xander-from-getting-fucked-not-even-the-heavy-rain-brazzers-xhLXHLh", + "review": 0, + "should_download": 0, + "title": "Nichts kann verhindern, dass hottie angel youngs und xander davon abhalten, gefickt zu werden, nicht einmal der schwere Regen - BRAZZERS | xHamster", + "file_name": "Nichts kann verhindern, dass hottie angel youngs und xander davon abhalten, gefickt zu werden, nicht einmal der schwere Regen - BRAZZERS [xhLXHLh].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f490aea8-3051-4eb6-bc66-5e7c9f83df12.mp4" + }, + { + "id": "f4bf9363-e8bc-4541-9cb6-12caba03fcd0", + "created_date": "2024-07-25 07:29:44.853500", + "last_modified_date": "2024-07-25 07:29:44.853500", + "version": 0, + "url": "https://ge.xhamster.com/videos/my-stepmother-and-my-stepsister-are-deep-throat-fucked-by-four-of-my-best-friends-xhhWhhJ", + "review": 0, + "should_download": 0, + "title": "Meine stiefmutter und meine stiefschwester werden von vier meiner besten freunde tief in den hals gefickt! | xHamster", + "file_name": "Meine stiefmutter und meine stiefschwester werden von vier meiner besten freunde tief in den hals gefickt! [xhhWhhJ].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/f4bf9363-e8bc-4541-9cb6-12caba03fcd0.mp4" + }, + { + "id": "f4ff15aa-47d0-493a-8048-3da1a986aa1c", + "created_date": "2024-07-25 07:29:47.351700", + "last_modified_date": "2024-07-25 07:29:47.351700", + "version": 0, + "url": "https://ge.xhamster.com/videos/soapy-stepsisters-part-1-xhn84pZ", + "review": 0, + "should_download": 0, + "title": "Seifige Stiefschwestern Teil 1 | xHamster", + "file_name": "Seifige Stiefschwestern Teil 1 [xhn84pZ].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f4ff15aa-47d0-493a-8048-3da1a986aa1c.mp4" + }, + { + "id": "f52623c9-6e17-4eb4-80da-e1ca2b62582b", + "created_date": "2024-07-25 07:29:46.691414", + "last_modified_date": "2024-07-25 07:29:46.691414", + "version": 0, + "url": "https://ge.xhamster.com/videos/a-model-family-full-movie-xh2xAvX", + "review": 0, + "should_download": 0, + "title": "Eine Modellfamilie! (kompletter Film) | xHamster", + "file_name": "Eine Modellfamilie! (kompletter Film) [xh2xAvX].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f52623c9-6e17-4eb4-80da-e1ca2b62582b.mp4" + }, + { + "id": "f572c079-c845-4112-8bd2-fb0dd0629e75", + "created_date": "2024-07-25 07:29:44.921631", + "last_modified_date": "2024-07-25 07:29:44.921631", + "version": 0, + "url": "https://ge.xhamster.com/videos/desideria-episode-3-xh1HfiY", + "review": 0, + "should_download": 0, + "title": "Desideria - Episode 3, Free Romantic HD Porn 04 | xHamster", + "file_name": "Desideria - Episode 3 [xh1HfiY].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f572c079-c845-4112-8bd2-fb0dd0629e75.mp4" + }, + { + "id": "f580ebd8-b1da-45a8-ac41-773708bf2a7e", + "created_date": "2024-11-10 16:53:33.483206", + "last_modified_date": "2024-11-10 16:53:33.483206", + "version": 0, + "url": "https://ge.xhamster.com/videos/caligola-la-storia-mai-raccontata-hd-10028296", + "review": 0, + "should_download": 0, + "title": "Caligola - La Storia Mai Raccontata HD, Porn fd | xHamster", + "file_name": "Caligola - La Storia Mai Raccontata (HD) [10028296].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/f580ebd8-b1da-45a8-ac41-773708bf2a7e.mp4" + }, + { + "id": "f5878d71-7ea5-4df5-adff-487194279b93", + "created_date": "2024-10-21 15:08:43.548046", + "last_modified_date": "2024-10-21 16:33:54.964000", + "version": 1, + "url": "https://ge.xhamster.com/videos/gangbang-in-willi-s-bar-xhI0ys2", + "review": 0, + "should_download": 0, + "title": "Gangbang in Willis Bar | xHamster", + "file_name": "Gangbang in Willis Bar [xhI0ys2].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/f5878d71-7ea5-4df5-adff-487194279b93.mp4" + }, + { + "id": "f5cb4ee1-bbd2-49e8-9d9f-c1ac3b48ebf2", + "created_date": "2024-07-25 07:29:47.355166", + "last_modified_date": "2024-07-25 07:29:47.355166", + "version": 0, + "url": "https://ge.xhamster.com/videos/first-time-double-vaginal-take-it-easy-on-guys-5956121", + "review": 0, + "should_download": 0, + "title": "Zum ersten Mal doppelt vaginal, nimm es einfach mit Jungs | xHamster", + "file_name": "Zum ersten Mal doppelt vaginal, nimm es einfach mit Jungs [5956121].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f5cb4ee1-bbd2-49e8-9d9f-c1ac3b48ebf2.mp4" + }, + { + "id": "f5d60aac-d5dc-4d93-893a-b66f7c6cde79", + "created_date": "2024-07-25 07:29:47.032147", + "last_modified_date": "2024-07-25 07:29:47.032147", + "version": 0, + "url": "https://ge.xhamster.com/videos/nice-beach-holyday-9620808", + "review": 0, + "should_download": 0, + "title": "Sch\u00f6ner Strandurlaub | xHamster", + "file_name": "Sch\u00f6ner Strandurlaub [9620808].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f5d60aac-d5dc-4d93-893a-b66f7c6cde79.mp4" + }, + { + "id": "f62d4a5f-cbf7-45a2-b8a7-81bad7731ddb", + "created_date": "2024-07-25 07:29:45.363987", + "last_modified_date": "2024-07-25 07:29:45.363987", + "version": 0, + "url": "https://ge.xhamster.com/videos/busty-ginger-stepdaughter-with-braces-needs-cum-on-her-tits-13991028", + "review": 0, + "should_download": 0, + "title": "Vollbusige rothaarige Stieftochter mit Zahnspange braucht Sperma auf ihre Titten | xHamster", + "file_name": "Vollbusige rothaarige Stieftochter mit Zahnspange braucht Sperma auf ihre Titten [13991028].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f62d4a5f-cbf7-45a2-b8a7-81bad7731ddb.mp4" + }, + { + "id": "f63f1ad1-bc42-4fa0-ab06-65bfa1a9cbd6", + "created_date": "2024-07-25 07:29:48.006530", + "last_modified_date": "2024-07-25 07:29:48.006530", + "version": 0, + "url": "https://ge.xhamster.com/videos/wife-explores-sex-of-all-kinds-10739198", + "review": 0, + "should_download": 0, + "title": "Ehefrau erforscht Sex aller Art | xHamster", + "file_name": "Ehefrau erforscht Sex aller Art [10739198].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f63f1ad1-bc42-4fa0-ab06-65bfa1a9cbd6.mp4" + }, + { + "id": "f6496ffb-dca6-45cb-96a2-24198ea347c5", + "created_date": "2024-07-25 07:29:45.563668", + "last_modified_date": "2024-07-25 07:29:45.563668", + "version": 0, + "url": "https://ge.xhamster.com/videos/mary-jane-1972-us-full-movie-rhonda-king-dvd-rip-xh6AWpx", + "review": 0, + "should_download": 0, + "title": "Mary Jane (1972, US, kompletter Film, Rhonda King, DVD Rip) | xHamster", + "file_name": "Mary Jane (1972, US, kompletter Film, Rhonda King, DVD Rip) [xh6AWpx].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/f6496ffb-dca6-45cb-96a2-24198ea347c5.mp4" + }, + { + "id": "f657f146-e3e7-45b7-89a9-af1c049e135c", + "created_date": "2024-07-25 07:29:45.442335", + "last_modified_date": "2024-07-25 07:29:45.442335", + "version": 0, + "url": "https://ge.xhamster.com/videos/hello-wild-90s-we-miss-you-8842932", + "review": 0, + "should_download": 0, + "title": "Hallo, wilde 90er, wir vermissen dich! | xHamster", + "file_name": "Hallo, wilde 90er, wir vermissen dich! [8842932].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f657f146-e3e7-45b7-89a9-af1c049e135c.mp4" + }, + { + "id": "f667b81c-6202-4711-ac5d-d755b9179a04", + "created_date": "2024-07-25 07:29:45.540029", + "last_modified_date": "2024-07-25 07:29:45.540029", + "version": 0, + "url": "https://ge.xhamster.com/videos/husband-and-his-friend-fuck-wife-on-hike-in-the-desert-giving-her-a-double-creampie-xhlhBvy", + "review": 0, + "should_download": 0, + "title": "Ehemann und sein Freund ficken Ehefrau auf Wanderung in der W\u00fcste und geben ihr einen doppelten Creampie | xHamster", + "file_name": "Ehemann und sein Freund ficken Ehefrau auf Wanderung in der W\u00fcste und geben ihr einen doppelten Creampie [xhlhBvy].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/f667b81c-6202-4711-ac5d-d755b9179a04.mp4" + }, + { + "id": "f68d0b56-59d8-43d3-9aaa-72f2ced1dd3d", + "created_date": "2024-12-29 23:53:27.950469", + "last_modified_date": "2024-12-29 23:53:27.950469", + "version": 0, + "url": "https://ge.xhamster.com/videos/sister-porn-14231224", + "review": 0, + "should_download": 0, + "title": "Schwester-Porno | xHamster", + "file_name": "Schwester-Porno [14231224].mp4", + "path": null, + "cloud_link": "/media/tpeetz/media1/f68d0b56-59d8-43d3-9aaa-72f2ced1dd3d.mp4" + }, + { + "id": "f696911d-718d-4b0b-bcbc-55fe334fa457", + "created_date": "2024-07-25 07:29:46.849943", + "last_modified_date": "2024-07-25 07:29:46.849943", + "version": 0, + "url": "https://ge.xhamster.com/videos/lets-get-nasty-full-original-movie-xhW6wcp", + "review": 0, + "should_download": 0, + "title": "Lass uns b\u00f6se werden (kompletter Originalfilm) | xHamster", + "file_name": "Lass uns b\u00f6se werden (kompletter Originalfilm) [xhW6wcp].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f696911d-718d-4b0b-bcbc-55fe334fa457.mp4" + }, + { + "id": "f698823e-7494-4f7a-a030-6b4a85fb5e84", + "created_date": "2024-11-01 21:08:26.929393", + "last_modified_date": "2024-11-01 21:08:26.929393", + "version": 0, + "url": "https://ge.xhamster.com/videos/school-girl-fucks-two-guys-after-school-10311343", + "review": 0, + "should_download": 0, + "title": "Schulm\u00e4dchen fickt zwei Typen nach der Schule | xHamster", + "file_name": "Schulm\u00e4dchen fickt zwei Typen nach der Schule [10311343].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/f698823e-7494-4f7a-a030-6b4a85fb5e84.mp4" + }, + { + "id": "f6a5a528-6a0a-4854-9840-7892177eae90", + "created_date": "2024-07-25 07:29:46.098955", + "last_modified_date": "2024-07-25 07:29:46.098955", + "version": 0, + "url": "https://ge.xhamster.com/videos/familystrokes-hot-stepmom-teaches-her-teens-how-to-fuck-12705727", + "review": 0, + "should_download": 0, + "title": "Familystrokes - eine hei\u00dfe Stiefmutter bringt ihrem Teenager bei, wie man fickt | xHamster", + "file_name": "Familystrokes - eine hei\u00dfe Stiefmutter bringt ihrem Teenager bei, wie man fickt [12705727].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f6a5a528-6a0a-4854-9840-7892177eae90.mp4" + }, + { + "id": "f6ea70f6-4099-4176-aa68-15a24dcedccf", + "created_date": "2024-10-07 20:47:56.422794", + "last_modified_date": "2024-10-21 16:34:00.223000", + "version": 1, + "url": "https://ge.xhamster.com/videos/teen-blonde-caught-cheating-and-fucked-in-the-ass-xhFkr1m", + "review": 0, + "should_download": 0, + "title": "Teenie-blondine erwischt beim betr\u00fcgen und wurde in den arsch gefickt | xHamster", + "file_name": "Teenie-blondine erwischt beim betr\u00fcgen und wurde in den arsch gefickt [xhFkr1m].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/f6ea70f6-4099-4176-aa68-15a24dcedccf.mp4" + }, + { + "id": "f70776d5-2863-4a6c-a952-61c567648dd4", + "created_date": "2024-07-25 07:29:47.422280", + "last_modified_date": "2025-01-03 11:56:56.069000", + "version": 1, + "url": "https://ge.xhamster.com/videos/wine-and-group-sex-party-1075537", + "review": 0, + "should_download": 0, + "title": "Wein- und Gruppensex-Party | xHamster", + "file_name": "Wein- und Gruppensex-Party [1075537].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f70776d5-2863-4a6c-a952-61c567648dd4.mp4" + }, + { + "id": "f7133fd5-f05b-4c9c-ab74-308a683227f8", + "created_date": "2024-07-25 07:29:45.336750", + "last_modified_date": "2024-07-25 07:29:45.336750", + "version": 0, + "url": "https://ge.xhamster.com/videos/thirsty-babes-for-cock-looking-on-the-road-for-some-adventure-xhR0S3U", + "review": 0, + "should_download": 0, + "title": "Durstige Sch\u00e4tzchen f\u00fcr Schwanz suchen auf der Stra\u00dfe nach einem Abenteuer | xHamster", + "file_name": "Durstige Sch\u00e4tzchen f\u00fcr Schwanz suchen auf der Stra\u00dfe nach einem Abenteuer [xhR0S3U].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f7133fd5-f05b-4c9c-ab74-308a683227f8.mp4" + }, + { + "id": "f75b7fe6-ad33-4d62-9c15-fac12d73c041", + "created_date": "2024-07-25 07:29:47.435142", + "last_modified_date": "2024-07-25 07:29:47.435142", + "version": 0, + "url": "https://ge.xhamster.com/videos/2-guys-caught-german-girl-in-public-and-picked-her-up-for-a-threesome-mmf-xhblDyR", + "review": 0, + "should_download": 0, + "title": "2 M\u00e4nner erwischen deutsche teen am See und \u00fcberreden sie zum dreier | xHamster", + "file_name": "2 M\u00e4nner erwischen deutsche teen am See und \u00fcberreden sie zum dreier [xhblDyR].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f75b7fe6-ad33-4d62-9c15-fac12d73c041.mp4" + }, + { + "id": "f79b0cbe-4ad7-415c-86d0-658d2ade0ed6", + "created_date": "2024-07-25 07:29:47.646537", + "last_modified_date": "2024-07-25 07:29:47.646537", + "version": 0, + "url": "https://ge.xhamster.com/videos/help-me-suck-your-stepbros-cock-12723528", + "review": 0, + "should_download": 0, + "title": "Hilf mir, den Schwanz deines Stiefbruders zu lutschen | xHamster", + "file_name": "Hilf mir, den Schwanz deines Stiefbruders zu lutschen [12723528].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f79b0cbe-4ad7-415c-86d0-658d2ade0ed6.mp4" + }, + { + "id": "f7e2a208-3323-415e-8e28-b2ab9a002128", + "created_date": "2024-07-25 07:29:45.135471", + "last_modified_date": "2024-07-25 07:29:45.135471", + "version": 0, + "url": "https://ge.xhamster.com/videos/klassengeile-1977-6317304", + "review": 0, + "should_download": 0, + "title": "Klassengeile 1977: Free Orgy Porn Video 2e | xHamster", + "file_name": "Klassengeile (1977) [6317304].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f7e2a208-3323-415e-8e28-b2ab9a002128.mp4" + }, + { + "id": "f805dcca-513b-42f0-b516-84650a3536cd", + "created_date": "2024-07-25 07:29:46.166274", + "last_modified_date": "2024-07-25 07:29:46.166274", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-boss-is-fucking-the-secretary-xhIvgCm", + "review": 0, + "should_download": 0, + "title": "Der Chef fickt die Sekret\u00e4rin | xHamster", + "file_name": "Der Chef fickt die Sekret\u00e4rin [xhIvgCm].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f805dcca-513b-42f0-b516-84650a3536cd.mp4" + }, + { + "id": "f8351b1b-b554-4d24-851e-a54e5e89eadb", + "created_date": "2024-07-25 07:29:45.382600", + "last_modified_date": "2024-07-25 07:29:45.382600", + "version": 0, + "url": "https://ge.xhamster.com/videos/nervous-couple-end-up-double-teaming-busty-masseuse-xhJ8Fbx", + "review": 0, + "should_download": 0, + "title": "Nerv\u00f6se Paare landen im Doppel, vollbusige Masseuse | xHamster", + "file_name": "Nerv\u00f6se Paare landen im Doppel, vollbusige Masseuse [xhJ8Fbx].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f8351b1b-b554-4d24-851e-a54e5e89eadb.mp4" + }, + { + "id": "f844a849-973b-4452-8402-73e2121ed015", + "created_date": "2024-07-25 07:29:46.641562", + "last_modified_date": "2024-07-25 07:29:46.641562", + "version": 0, + "url": "https://ge.xhamster.com/videos/momsteachsex-my-bf-caught-punished-by-step-mom-6876583", + "review": 0, + "should_download": 0, + "title": "Momsteachsex - mein Freund von Stiefmutter erwischt und bestraft | xHamster", + "file_name": "Momsteachsex - mein Freund von Stiefmutter erwischt und bestraft [6876583].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f844a849-973b-4452-8402-73e2121ed015.mp4" + }, + { + "id": "f897067a-c74c-4bf5-960e-926d78a74ef0", + "created_date": "2024-07-25 07:29:46.733090", + "last_modified_date": "2025-01-03 14:38:52.127000", + "version": 2, + "url": "https://ge.xhamster.com/videos/stiev-sohn-hynotiersiert-und-fick-glaeubige-stievmutte-xhWYq0y", + "review": 0, + "should_download": 0, + "title": "Video wurde gel\u00f6scht", + "file_name": "stiev sohn hynotiersiert und fick Glaeubige stievmutte [xhWYq0y].mp4", + "path": "/mnt/e/media", + "cloud_link": "" + }, + { + "id": "f8e0543f-e1e1-4a65-b486-d35ec497c31c", + "created_date": "2024-07-25 07:29:46.837259", + "last_modified_date": "2024-07-25 07:29:46.837259", + "version": 0, + "url": "https://ge.xhamster.com/videos/classic-bride-comforters-230164", + "review": 0, + "should_download": 0, + "title": "Classic - Brautdecken | xHamster", + "file_name": "Classic - Brautdecken [230164].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f8e0543f-e1e1-4a65-b486-d35ec497c31c.mp4" + }, + { + "id": "f8e37b1f-1cdd-4100-9968-a2f1ca2de6cd", + "created_date": "2024-07-25 07:29:44.345173", + "last_modified_date": "2024-07-25 07:29:44.345173", + "version": 0, + "url": "https://ge.xhamster.com/videos/busty-redhead-annabel-redd-rides-roommate-s-big-cock-s14-e12-xhWwhdk", + "review": 0, + "should_download": 0, + "title": "Die vollbusige rothaarige Annabel Redd reitet den gro\u00dfen Schwanz des Mitbewohners - s14: e12 | xHamster", + "file_name": "Die vollbusige rothaarige Annabel Redd reitet den gro\u00dfen Schwanz des Mitbewohners - s14\uff1a e12 [xhWwhdk].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f8e37b1f-1cdd-4100-9968-a2f1ca2de6cd.mp4" + }, + { + "id": "f966d568-677c-4d59-91f5-8daefa272e7d", + "created_date": "2024-07-25 07:29:47.653796", + "last_modified_date": "2024-07-25 07:29:47.653796", + "version": 0, + "url": "https://ge.xhamster.com/videos/french-family-reunion-part-iv-529075", + "review": 0, + "should_download": 0, + "title": "Franz\u00f6sisches Familientreffen, Teil iv | xHamster", + "file_name": "Franz\u00f6sisches Familientreffen, Teil iv [529075].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f966d568-677c-4d59-91f5-8daefa272e7d.mp4" + }, + { + "id": "f97895b4-2e10-47c5-af1b-9d037da0adfe", + "created_date": "2024-07-25 07:29:46.885388", + "last_modified_date": "2024-07-25 07:29:46.885388", + "version": 0, + "url": "https://ge.xhamster.com/videos/student-orgy-college-girls-gone-wild-xhqBUKV", + "review": 0, + "should_download": 0, + "title": "Studentenorgie: college-m\u00e4dchen, die wild geworden sind | xHamster", + "file_name": "Studentenorgie\uff1a college-m\u00e4dchen, die wild geworden sind [xhqBUKV].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f97895b4-2e10-47c5-af1b-9d037da0adfe.mp4" + }, + { + "id": "f9ce1560-d97f-426b-8ec1-8aeb4e71333e", + "created_date": "2024-07-25 07:29:47.718551", + "last_modified_date": "2024-07-25 07:29:47.718551", + "version": 0, + "url": "https://ge.xhamster.com/videos/stepsis-kenzie-reeves-says-i-just-need-some-dick-xhy1F62", + "review": 0, + "should_download": 0, + "title": "Stiefschwester Kenzie Reeves sagt, ich brauche nur einen Schwanz! | xHamster", + "file_name": "Stiefschwester Kenzie Reeves sagt, ich brauche nur einen Schwanz! [xhy1F62].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/f9ce1560-d97f-426b-8ec1-8aeb4e71333e.mp4" + }, + { + "id": "fb0fb178-b5f0-4665-8727-c7845a202907", + "created_date": "2024-11-10 16:53:33.477460", + "last_modified_date": "2024-11-10 16:53:33.477460", + "version": 0, + "url": "https://ge.xhamster.com/videos/initiation-of-young-lady-1979-12425395", + "review": 0, + "should_download": 0, + "title": "Initiation einer jungen Dame (1979) | xHamster", + "file_name": "Initiation einer jungen Dame (1979) [12425395].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/fb0fb178-b5f0-4665-8727-c7845a202907.mp4" + }, + { + "id": "fb2f2077-777d-464c-b11d-a30e3dbdaeee", + "created_date": "2024-07-25 07:29:45.696082", + "last_modified_date": "2024-07-25 07:29:45.696082", + "version": 0, + "url": "https://ge.xhamster.com/videos/please-gangbang-my-stepsister-episode-01-xhu0fCk", + "review": 0, + "should_download": 0, + "title": "Bitte, Gangbang meiner Stiefschwester !!! - Episode # 01 | xHamster", + "file_name": "Bitte, Gangbang meiner Stiefschwester !!! - Episode # 01 [xhu0fCk].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fb2f2077-777d-464c-b11d-a30e3dbdaeee.mp4" + }, + { + "id": "fb440beb-0c50-46de-ae69-0ca47a6711ef", + "created_date": "2024-07-25 07:29:46.507343", + "last_modified_date": "2024-07-25 07:29:46.507343", + "version": 0, + "url": "https://ge.xhamster.com/videos/massive-cock-dorm-fling-threesome-in-shared-accommodation-xhIRQWT", + "review": 0, + "should_download": 0, + "title": "Massiver schwanz-schlafsaal Fling: dreier in einer wohngemeinschaft | xHamster", + "file_name": "Massiver schwanz-schlafsaal Fling\uff1a dreier in einer wohngemeinschaft [xhIRQWT].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fb440beb-0c50-46de-ae69-0ca47a6711ef.mp4" + }, + { + "id": "fb58cc85-0931-4981-8fe6-c39be21e83ca", + "created_date": "2024-08-28 23:21:54.348385", + "last_modified_date": "2024-08-28 23:21:54.348385", + "version": 0, + "url": "https://ge.xhamster.com/videos/brother-wins-the-bet-10876076", + "review": 0, + "should_download": 0, + "title": "Bruder gewinnt die Wette | xHamster", + "file_name": "Bruder gewinnt die Wette [10876076].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/fb58cc85-0931-4981-8fe6-c39be21e83ca.mp4" + }, + { + "id": "fb7f20ff-d141-4d35-956a-980abdbbf593", + "created_date": "2024-07-25 07:29:47.524845", + "last_modified_date": "2025-01-03 11:57:04.163000", + "version": 1, + "url": "https://ge.xhamster.com/videos/bad-stepmom-s20-e7-xhrubvP", + "review": 0, + "should_download": 0, + "title": "Bad Stepmom - S20 E7: Taboo HD Porn Video c2 | xHamster", + "file_name": "B\u00f6se stiefmutter - s20\uff1a e7 [xhrubvP].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fb7f20ff-d141-4d35-956a-980abdbbf593.mp4" + }, + { + "id": "fb802800-25f8-4f13-970b-bc66d87e0c7b", + "created_date": "2024-07-25 07:29:47.914687", + "last_modified_date": "2024-07-25 07:29:47.914687", + "version": 0, + "url": "https://ge.xhamster.com/videos/wild-holidays-episode-5-xhC9pvW", + "review": 0, + "should_download": 0, + "title": "Wilde Feiertage - Episode 5 | xHamster", + "file_name": "Wilde Feiertage - Episode 5 [xhC9pvW].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fb802800-25f8-4f13-970b-bc66d87e0c7b.mp4" + }, + { + "id": "fba4c507-aa26-4ea0-8b04-33d3563e0e01", + "created_date": "2024-07-25 07:29:44.636229", + "last_modified_date": "2024-07-25 07:29:44.636229", + "version": 0, + "url": "https://ge.xhamster.com/videos/familie-4216589", + "review": 0, + "should_download": 0, + "title": "Familie: Hardcore & Old & Young Porn Video 2a | xHamster", + "file_name": "Familie [4216589].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fba4c507-aa26-4ea0-8b04-33d3563e0e01.mp4" + }, + { + "id": "fbf0b604-a7f8-4a29-9375-b42af2d565a9", + "created_date": "2024-07-25 07:29:44.674205", + "last_modified_date": "2024-07-25 07:29:44.674205", + "version": 0, + "url": "https://ge.xhamster.com/videos/soaking-step-milfs-pussy-caitlin-bell-xhdJGFF", + "review": 0, + "should_download": 0, + "title": "Tr\u00e4nken in die Muschi der Stiefmutter, klingelt | xHamster", + "file_name": "Tr\u00e4nken in die Muschi der Stiefmutter, klingelt [xhdJGFF].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fbf0b604-a7f8-4a29-9375-b42af2d565a9.mp4" + }, + { + "id": "fc0f835c-c668-4f46-8fea-ffd671cbf813", + "created_date": "2024-07-25 07:29:45.467769", + "last_modified_date": "2024-07-25 07:29:45.467769", + "version": 0, + "url": "https://ge.xhamster.com/videos/snow-white-original-hd-11553372", + "review": 0, + "should_download": 0, + "title": "Schneewittchen Original hd | xHamster", + "file_name": "Schneewittchen Original hd [11553372].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fc0f835c-c668-4f46-8fea-ffd671cbf813.mp4" + }, + { + "id": "fc2a73ae-21d8-443a-b54e-491254b3706a", + "created_date": "2024-08-28 23:21:54.368688", + "last_modified_date": "2024-08-28 23:21:54.368688", + "version": 0, + "url": "https://ge.xhamster.com/videos/vintage-1960s-summer-camp-memories-4176629", + "review": 0, + "should_download": 0, + "title": "Retro - 1960er - Sommerlager-Erinnerungen | xHamster", + "file_name": "Retro - 1960er - Sommerlager-Erinnerungen [4176629].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/fc2a73ae-21d8-443a-b54e-491254b3706a.mp4" + }, + { + "id": "fc6531da-e50b-42f1-b87e-24e64634f999", + "created_date": "2024-07-25 07:29:46.539346", + "last_modified_date": "2024-07-25 07:29:46.539346", + "version": 0, + "url": "https://ge.xhamster.com/videos/work-sex-1979-vintage-german-full-movie-better-quality-xhvwuh8", + "review": 0, + "should_download": 0, + "title": "Arbeitszeit (Germany 1979, full movie) | xHamster", + "file_name": "Arbeitszeit (Germany 1979, full movie) [xhvwuh8].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fc6531da-e50b-42f1-b87e-24e64634f999.mp4" + }, + { + "id": "fc7f64a7-604f-4c9c-bcaf-5ee8a75202a9", + "created_date": "2024-07-25 07:29:48.101694", + "last_modified_date": "2024-07-25 07:29:48.101694", + "version": 0, + "url": "https://ge.xhamster.com/videos/dont-worry-well-get-you-that-sperm-swapmom-jessica-starling-tells-swapdaughter-harley-king-s7-e3-xh0hShz", + "review": 0, + "should_download": 0, + "title": "\"Keine Sorgen, wir bekommen dich dieses Sperma!\" Swapmom jessica Starling sagt swapdaughter harley King-S7: E3 | xHamster", + "file_name": "\uff02Keine Sorgen, wir bekommen dich dieses Sperma!\uff02 Swapmom jessica Starling sagt swapdaughter harley King-S7\uff1a E3 [xh0hShz].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fc7f64a7-604f-4c9c-bcaf-5ee8a75202a9.mp4" + }, + { + "id": "fca48c73-0ffd-485c-86b3-0ae4f72d6b03", + "created_date": "2024-07-25 07:29:46.001109", + "last_modified_date": "2024-07-25 07:29:46.001109", + "version": 0, + "url": "https://ge.xhamster.com/videos/stossverkehr-im-schulbus-xhyJwKf", + "review": 0, + "should_download": 0, + "title": "Stossverkehr Im Schulbus, Free European Porn 53 | xHamster", + "file_name": "Stossverkehr Im Schulbus [xhyJwKf].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fca48c73-0ffd-485c-86b3-0ae4f72d6b03.mp4" + }, + { + "id": "fcb9ed0d-e45a-4eeb-a799-9168ef957564", + "created_date": "2024-11-01 21:08:26.933919", + "last_modified_date": "2024-11-01 21:08:26.933919", + "version": 0, + "url": "https://ge.xhamster.com/videos/wette-verloren-jetzt-ficken-wir-dich-xh8grFD", + "review": 0, + "should_download": 0, + "title": "Wette Verloren Jetzt Ficken Wir Dich, HD Porn 95 | xHamster", + "file_name": "Wette verloren jetzt ficken wir dich [xh8grFD].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/fcb9ed0d-e45a-4eeb-a799-9168ef957564.mp4" + }, + { + "id": "fd42defb-2033-4be7-8af6-d6f8322944a3", + "created_date": "2024-07-25 07:29:45.036684", + "last_modified_date": "2024-07-25 07:29:45.036684", + "version": 0, + "url": "https://ge.xhamster.desi/videos/mags-choice-1-8908123", + "review": 0, + "should_download": 0, + "title": "Mags Wahl 1 | xHamster", + "file_name": "Mags Wahl 1 [8908123].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/Media/fd42defb-2033-4be7-8af6-d6f8322944a3.mp4" + }, + { + "id": "fd5aa40b-dfa5-4b00-8efc-6d85785a9fa3", + "created_date": "2024-07-25 07:29:47.276456", + "last_modified_date": "2024-07-25 07:29:47.276456", + "version": 0, + "url": "https://ge.xhamster.com/videos/sommertraume-junger-madchen-nr-1-full-movie-xhOmiqY", + "review": 0, + "should_download": 0, + "title": "Sommertr\u00e4ume junger m\u00e4dchen # 1 (kompletter film) | xHamster", + "file_name": "Sommertr\u00e4ume junger m\u00e4dchen # 1 (kompletter film) [xhOmiqY].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fd5aa40b-dfa5-4b00-8efc-6d85785a9fa3.mp4" + }, + { + "id": "fd6c50f1-20f1-4387-94e9-f380f22eb202", + "created_date": "2024-07-25 07:29:45.786112", + "last_modified_date": "2024-07-25 07:29:45.786112", + "version": 0, + "url": "https://ge.xhamster.com/videos/schulmadchen-6-schamlos-nach-der-schule-xhd5zZ3", + "review": 0, + "should_download": 0, + "title": "Schulmadchen 6 - Schamlos Nach Der Schule: Free HD Porn 61 | xHamster", + "file_name": "Schulmadchen 6 - Schamlos Nach der Schule [xhd5zZ3].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fd6c50f1-20f1-4387-94e9-f380f22eb202.mp4" + }, + { + "id": "fd845e43-b85e-4903-9b04-6ee541d9adeb", + "created_date": "2024-07-25 07:29:46.629458", + "last_modified_date": "2024-07-25 07:29:46.629458", + "version": 0, + "url": "https://ge.xhamster.com/videos/cousin-pauline-1973-mkx-13752127", + "review": 0, + "should_download": 0, + "title": "Cousin Pauline - MKX | xHamster", + "file_name": "Cousin Pauline - MKX [13752127].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fd845e43-b85e-4903-9b04-6ee541d9adeb.mp4" + }, + { + "id": "fd86bdde-9954-423e-a0ce-11a33a03ba0a", + "created_date": "2024-07-25 07:29:46.572455", + "last_modified_date": "2024-07-25 07:29:46.572455", + "version": 0, + "url": "https://ge.xhamster.com/videos/fun-with-farmgirls-1993-13175749", + "review": 0, + "should_download": 0, + "title": "Spa\u00df mit Bauernm\u00e4dchen (1993) | xHamster", + "file_name": "Spa\u00df mit Bauernm\u00e4dchen (1993) [13175749].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fd86bdde-9954-423e-a0ce-11a33a03ba0a.mp4" + }, + { + "id": "fd98ad5f-961d-4b59-a6d8-c2d78e19508a", + "created_date": "2024-07-25 07:29:44.447845", + "last_modified_date": "2024-07-25 07:29:44.447845", + "version": 0, + "url": "https://ge.xhamster.com/videos/why-jerk-off-when-you-have-a-stepmom-like-cory-chase-tabooheat-xhMjY1b", + "review": 0, + "should_download": 0, + "title": "Warum wichsen, wenn du eine Stiefmutter wie Cory Chase hast? - Tabu-Hitze | xHamster", + "file_name": "Warum wichsen, wenn du eine Stiefmutter wie Cory Chase hast\uff1f - Tabu-Hitze [xhMjY1b].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fd98ad5f-961d-4b59-a6d8-c2d78e19508a.mp4" + }, + { + "id": "fddfbe31-3ab6-46bc-b7ca-3194431e6466", + "created_date": "2024-07-25 07:29:47.536387", + "last_modified_date": "2025-01-03 01:51:00.369000", + "version": 1, + "url": "https://ge.xhamster.com/videos/erotic-adventures-of-candy-1978-4k-remastered-14845665", + "review": 0, + "should_download": 0, + "title": "Erotische Abenteuer von Candy 1978 (4k remastered) | xHamster", + "file_name": "Erotische Abenteuer von Candy 1978 (4k remastered) [14845665].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fddfbe31-3ab6-46bc-b7ca-3194431e6466.mp4" + }, + { + "id": "fe2aa3f5-e79b-4667-9b3e-c80d43ef0626", + "created_date": "2024-11-10 16:53:33.490208", + "last_modified_date": "2024-11-10 16:53:33.490208", + "version": 0, + "url": "https://ge.xhamster.com/videos/sibling-seductions-3-xh8x5tQ", + "review": 0, + "should_download": 0, + "title": "Geschwisterverf\u00fchrungen 3 | xHamster", + "file_name": "Geschwisterverf\u00fchrungen 3 [xh8x5tQ].mp4", + "path": null, + "cloud_link": "/media/tpeetz/Media/fe2aa3f5-e79b-4667-9b3e-c80d43ef0626.mp4" + }, + { + "id": "fe2b8720-35ac-4740-a7b5-7e661e0c816d", + "created_date": "2024-07-25 07:29:45.763339", + "last_modified_date": "2024-07-25 07:29:45.763339", + "version": 0, + "url": "https://ge.xhamster.com/videos/threesome-on-the-beach-in-a-fantastic-island-xhjgrOy", + "review": 0, + "should_download": 0, + "title": "Dreier am Strand, auf einer fantastischen Insel | xHamster", + "file_name": "Dreier am Strand, auf einer fantastischen Insel [xhjgrOy].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fe2b8720-35ac-4740-a7b5-7e661e0c816d.mp4" + }, + { + "id": "fe3cb40d-c50e-4751-93de-ebfcdc987333", + "created_date": "2024-07-25 07:29:47.191322", + "last_modified_date": "2024-07-25 07:29:47.191322", + "version": 0, + "url": "https://ge.xhamster.com/videos/you-want-to-bang-my-stepdad-gfs-kinky-fantasy-xhlmUZe", + "review": 0, + "should_download": 0, + "title": "\"Du willst meinen Stiefvater knallen !?\" Freundin ist versaut, Fantasie | xHamster", + "file_name": "\uff02Du willst meinen Stiefvater knallen !\uff1f\uff02 Freundin ist versaut, Fantasie [xhlmUZe].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fe3cb40d-c50e-4751-93de-ebfcdc987333.mp4" + }, + { + "id": "fe8d2a23-56dd-44a2-9e9d-3c17227989f7", + "created_date": "2024-07-25 07:29:47.415040", + "last_modified_date": "2024-07-25 07:29:47.415040", + "version": 0, + "url": "https://ge.xhamster.com/videos/a-welcomed-distraction-xhog9CP", + "review": 0, + "should_download": 0, + "title": "Eine willkommene Ablenkung | xHamster", + "file_name": "Eine willkommene Ablenkung [xhog9CP].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fe8d2a23-56dd-44a2-9e9d-3c17227989f7.mp4" + }, + { + "id": "fecc5dd1-0fca-408d-a56a-527df13ad701", + "created_date": "2024-07-25 07:29:44.662671", + "last_modified_date": "2024-07-25 07:29:44.662671", + "version": 0, + "url": "https://ge.xhamster.com/videos/jung-und-geil-12203504", + "review": 0, + "should_download": 0, + "title": "Jung Und Geil: Free Pissing Porn Video 31 | xHamster", + "file_name": "Jung und geil [12203504].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fecc5dd1-0fca-408d-a56a-527df13ad701.mp4" + }, + { + "id": "feedde9d-0c65-4a08-b160-3c4b864d7368", + "created_date": "2024-07-25 07:29:45.233926", + "last_modified_date": "2024-07-25 07:29:45.233926", + "version": 0, + "url": "https://ge.xhamster.com/videos/meine-familie-ich-episode-4-xheKFK1", + "review": 0, + "should_download": 0, + "title": "Meine Familie Ich - Episode 4, Free Big Cock HD Porn 7a | xHamster", + "file_name": "Meine familie ich - Episode 4 [xheKFK1].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/feedde9d-0c65-4a08-b160-3c4b864d7368.mp4" + }, + { + "id": "fef798f2-119d-4cdb-bc02-7ab16b62ba31", + "created_date": "2024-07-25 07:29:45.491472", + "last_modified_date": "2024-07-25 07:29:45.491472", + "version": 0, + "url": "https://ge.xhamster.com/videos/busty-secretary-loves-riding-her-bosss-dick-5512216", + "review": 0, + "should_download": 0, + "title": "Vollbusige Sekret\u00e4rin liebt es, den Schwanz ihres Chefs zu reiten | xHamster", + "file_name": "Vollbusige Sekret\u00e4rin liebt es, den Schwanz ihres Chefs zu reiten [5512216].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fef798f2-119d-4cdb-bc02-7ab16b62ba31.mp4" + }, + { + "id": "ff1be3b4-9945-43ab-9abd-46573a9f4c8f", + "created_date": "2024-07-25 07:29:46.749984", + "last_modified_date": "2024-07-25 07:29:46.749984", + "version": 0, + "url": "https://ge.xhamster.com/videos/american-college-xxx-vol-2-original-version-in-hd-xhJRfPP", + "review": 0, + "should_download": 0, + "title": "American College xxx - vol (2) - (Originalversion in hd) | xHamster", + "file_name": "American College xxx - vol (2) - (Originalversion in hd) [xhJRfPP].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ff1be3b4-9945-43ab-9abd-46573a9f4c8f.mp4" + }, + { + "id": "ff6c572c-4985-4750-89b3-fa48d740b50d", + "created_date": "2024-07-25 07:29:44.525976", + "last_modified_date": "2024-07-25 07:29:44.525976", + "version": 0, + "url": "https://ge.xhamster.com/videos/german-amateur-threesome-with-creampie-2649148", + "review": 0, + "should_download": 0, + "title": "Deutscher Amateur-Dreier mit Creampie! | xHamster", + "file_name": "Deutscher Amateur-Dreier mit Creampie! [2649148].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ff6c572c-4985-4750-89b3-fa48d740b50d.mp4" + }, + { + "id": "ffa60223-391b-48df-9beb-97a6a59ad32a", + "created_date": "2024-07-25 07:29:45.832144", + "last_modified_date": "2024-07-25 07:29:45.832144", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-fleet-foursome-1992-xhIwCQP", + "review": 0, + "should_download": 0, + "title": "Der Flotte Vierer (1992) | xHamster", + "file_name": "Der Flotte Vierer (1992) [xhIwCQP].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ffa60223-391b-48df-9beb-97a6a59ad32a.mp4" + }, + { + "id": "ffb92eff-a5fb-4a63-9c26-a650e1737aed", + "created_date": "2024-07-25 07:29:46.553621", + "last_modified_date": "2024-07-25 07:29:46.553621", + "version": 0, + "url": "https://ge.xhamster.com/videos/the-young-like-it-hot-1983-9276329", + "review": 0, + "should_download": 0, + "title": "Die Jungen m\u00f6gen es hei\u00df 1983 | xHamster", + "file_name": "Die Jungen m\u00f6gen es hei\u00df 1983 [9276329].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ffb92eff-a5fb-4a63-9c26-a650e1737aed.mp4" + }, + { + "id": "ffc2f801-d77a-4b9b-9a84-825f626e7c05", + "created_date": "2024-07-25 07:29:48.169209", + "last_modified_date": "2024-07-25 07:29:48.169209", + "version": 0, + "url": "https://ge.xhamster.com/videos/no-one-told-me-it-would-be-so-fun-xhPCC1n", + "review": 0, + "should_download": 0, + "title": "Niemand sagte mir, es w\u00fcrde so viel Spa\u00df machen | xHamster", + "file_name": "Niemand sagte mir, es w\u00fcrde so viel Spa\u00df machen [xhPCC1n].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/ffc2f801-d77a-4b9b-9a84-825f626e7c05.mp4" + }, + { + "id": "ffddd1e3-3aa3-4f4f-b8b0-a588db6516d8", + "created_date": "2025-01-19 13:42:31.458322", + "last_modified_date": "2025-01-19 13:42:31.458329", + "version": 0, + "url": "https://ge.xhamster.com/videos/cheating-girlfriend-caught-in-the-act-two-guys-pleasuring-ellie-with-her-boyfriend-on-the-phone-xhxq2LF", + "review": 0, + "should_download": 0, + "title": "Betr\u00fcgende freundin auf frischer tat erwischt: Zwei typen befriedigen Ellie mit ihrem freund am telefon | xHamster", + "file_name": "Betr\u00fcgende freundin auf frischer tat erwischt\uff1a Zwei typen befriedigen Ellie mit ihrem freund am telefon [xhxq2LF].mp4", + "path": null, + "cloud_link": null + }, + { + "id": "fff3e63e-7a45-4a9f-b69b-ff6597390bf8", + "created_date": "2024-07-25 07:29:46.354059", + "last_modified_date": "2024-07-25 07:29:46.354059", + "version": 0, + "url": "https://ge.xhamster.com/videos/alyssia-kent-gerson-denny-rained-out-babes-13199158", + "review": 0, + "should_download": 0, + "title": "Alyssia Kent Gerson Denny - geregnet - Sch\u00e4tzchen | xHamster", + "file_name": "Alyssia Kent Gerson Denny - geregnet - Sch\u00e4tzchen [13199158].mp4", + "path": "/mnt/e/media", + "cloud_link": "/media/tpeetz/media1/fff3e63e-7a45-4a9f-b69b-ff6597390bf8.mp4" + } + ], + "article_author": [], + "book": [], + "role": [ + { + "id": "05a186f6-36a2-4cce-8904-187301193937", + "created_date": "2024-10-21 15:20:05.848000", + "last_modified_date": "2024-10-21 15:20:05.848000", + "version": 0, + "name": "MEDIA" + }, + { + "id": "0e0a5291-e74b-4ef8-823a-103f731e58bf", + "created_date": "2024-08-16 23:58:34.457000", + "last_modified_date": "2024-08-16 23:58:34.457000", + "version": 0, + "name": "ROLE_USER" + }, + { + "id": "d975bc73-1c21-4344-a500-4a90440edf7d", + "created_date": "2024-08-16 23:58:37.140000", + "last_modified_date": "2024-08-16 23:58:37.140000", + "version": 0, + "name": "ROLE_MEDIA" + }, + { + "id": "dd6bad92-8ff0-4928-93ab-b356c75f2a81", + "created_date": "2024-08-16 23:58:34.408000", + "last_modified_date": "2024-08-16 23:58:34.408000", + "version": 0, + "name": "ROLE_ADMIN" + } + ], + "field_position": [ + { + "id": "0968459a-b649-4ed7-a3ee-c14172e93b0e", + "created_date": "2024-08-16 23:58:37.437000", + "last_modified_date": "2024-08-16 23:58:37.437000", + "version": 0, + "name": "Goalie", + "short_name": "G", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "0a2976b2-a6f5-4e87-86e0-97270f64958d", + "created_date": "2024-08-16 23:58:37.363000", + "last_modified_date": "2024-08-16 23:58:37.363000", + "version": 0, + "name": "Tight End", + "short_name": "TE", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "1a363e8b-c34c-4ef9-9677-6fd7b9bdf143", + "created_date": "2024-08-16 23:58:37.374000", + "last_modified_date": "2024-08-16 23:58:37.374000", + "version": 0, + "name": "Kicker", + "short_name": "K", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "1bb73f00-21d3-4fca-a5c5-7f31be3670a1", + "created_date": "2024-08-16 23:58:37.382000", + "last_modified_date": "2024-08-16 23:58:37.382000", + "version": 0, + "name": "Long Snapper", + "short_name": "LS", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "2561c3e9-e09b-42f3-98fc-7224ce8ea85e", + "created_date": "2024-08-16 23:58:37.443000", + "last_modified_date": "2024-08-16 23:58:37.443000", + "version": 0, + "name": "Center", + "short_name": "C", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "2c33f079-cfaf-44ab-91ae-d03411ba745b", + "created_date": "2024-08-16 23:58:37.378000", + "last_modified_date": "2024-08-16 23:58:37.378000", + "version": 0, + "name": "Kick Returner", + "short_name": "KR", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "32bae8cd-c01e-4a13-82cc-2e6699bb30b9", + "created_date": "2024-08-16 23:58:37.421000", + "last_modified_date": "2024-08-16 23:58:37.421000", + "version": 0, + "name": "Third Base", + "short_name": "3B", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "3303b76e-688c-4a4c-8f7e-f76b1df5f5de", + "created_date": "2024-08-16 23:58:37.371000", + "last_modified_date": "2024-08-16 23:58:37.371000", + "version": 0, + "name": "Defensive Back", + "short_name": "DB", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "3682a19c-52de-40d5-9307-126d4c579d69", + "created_date": "2024-08-16 23:58:37.361000", + "last_modified_date": "2024-08-16 23:58:37.361000", + "version": 0, + "name": "Wide Receiver", + "short_name": "WR", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "370a5eb5-07c1-4ebf-94a0-57377a395fe1", + "created_date": "2024-08-16 23:58:37.375000", + "last_modified_date": "2024-08-16 23:58:37.375000", + "version": 0, + "name": "Punter", + "short_name": "P", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "3b57d6a5-004f-4bbf-9c8f-81e58084a919", + "created_date": "2024-08-16 23:58:37.411000", + "last_modified_date": "2024-08-16 23:58:37.411000", + "version": 0, + "name": "Outside Linebacker", + "short_name": "OLB", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "4408073f-29ef-4910-be1f-a9979ec96005", + "created_date": "2024-08-16 23:58:37.427000", + "last_modified_date": "2024-08-16 23:58:37.427000", + "version": 0, + "name": "Right Field", + "short_name": "RF", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "4632ca65-3155-4cb1-ad56-6886b12ee99a", + "created_date": "2024-08-16 23:58:37.438000", + "last_modified_date": "2024-08-16 23:58:37.438000", + "version": 0, + "name": "Defense", + "short_name": "D", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "47d394b9-f0de-4eb5-a5e0-afcb26506810", + "created_date": "2024-08-16 23:58:37.414000", + "last_modified_date": "2024-08-16 23:58:37.414000", + "version": 0, + "name": "Strong Safety", + "short_name": "SS", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "54aedda1-1732-4f14-98f4-0aecc924df15", + "created_date": "2024-08-16 23:58:37.364000", + "last_modified_date": "2024-08-16 23:58:37.364000", + "version": 0, + "name": "Fullback", + "short_name": "FB", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "598b3b35-6aa7-4aee-8299-f027bac4848b", + "created_date": "2024-08-16 23:58:37.406000", + "last_modified_date": "2024-08-16 23:58:37.406000", + "version": 0, + "name": "Cornerback", + "short_name": "CB", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "66623356-c8ce-4d7f-8e93-8c950fc28a0b", + "created_date": "2024-08-16 23:58:37.431000", + "last_modified_date": "2024-08-16 23:58:37.431000", + "version": 0, + "name": "Shooting Guard", + "short_name": "SG", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "69904927-8308-4099-910f-a786163334b9", + "created_date": "2024-08-16 23:58:37.426000", + "last_modified_date": "2024-08-16 23:58:37.426000", + "version": 0, + "name": "Center Field", + "short_name": "CF", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "73f6ded0-d803-4125-9adc-2c3cca3d4019", + "created_date": "2024-08-16 23:58:37.434000", + "last_modified_date": "2024-08-16 23:58:37.434000", + "version": 0, + "name": "Power Forward", + "short_name": "PF", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "7d265c03-d616-491b-a354-b1bda4bd410b", + "created_date": "2024-08-16 23:58:37.384000", + "last_modified_date": "2024-08-16 23:58:37.384000", + "version": 0, + "name": "Right Guard", + "short_name": "RG", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "90f07094-5f90-41cb-99f3-70efaf34c68e", + "created_date": "2024-08-16 23:58:37.435000", + "last_modified_date": "2024-08-16 23:58:37.435000", + "version": 0, + "name": "Center", + "short_name": "C", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "912415e0-6eb8-4baa-8e38-58514b49816a", + "created_date": "2024-08-16 23:58:37.429000", + "last_modified_date": "2024-08-16 23:58:37.429000", + "version": 0, + "name": "Point Guard", + "short_name": "PG", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "99e770eb-66c4-4b4d-bd95-d32ed7b818fd", + "created_date": "2024-08-16 23:58:37.441000", + "last_modified_date": "2024-08-16 23:58:37.441000", + "version": 0, + "name": "Right Wing", + "short_name": "RW", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "a313bf86-1347-42c4-83c0-fe3bd0b15aa0", + "created_date": "2024-08-16 23:58:37.412000", + "last_modified_date": "2024-08-16 23:58:37.412000", + "version": 0, + "name": "Inside Linebacker", + "short_name": "ILB", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "a37ab055-d340-4d6c-8fb3-2262c2372a42", + "created_date": "2024-08-16 23:58:37.417000", + "last_modified_date": "2024-08-16 23:58:37.417000", + "version": 0, + "name": "Catcher", + "short_name": "C", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "a5e483be-3f8a-44b9-81d8-4f767ba9a417", + "created_date": "2024-08-16 23:58:37.367000", + "last_modified_date": "2024-08-16 23:58:37.367000", + "version": 0, + "name": "Offensive Line", + "short_name": "OL", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "acb284fb-8917-4fbd-bd76-861a2636a3bc", + "created_date": "2024-08-16 23:58:37.424000", + "last_modified_date": "2024-08-16 23:58:37.424000", + "version": 0, + "name": "Left Field", + "short_name": "LF", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "adf598a1-d6b8-4b70-8d56-502cf731e95c", + "created_date": "2024-08-16 23:58:37.379000", + "last_modified_date": "2024-08-16 23:58:37.379000", + "version": 0, + "name": "Punt Returner", + "short_name": "PR", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "b43cdc34-5227-43fa-92f6-65c7ac5f6a64", + "created_date": "2024-08-16 23:58:37.432000", + "last_modified_date": "2024-08-16 23:58:37.432000", + "version": 0, + "name": "Small Forward", + "short_name": "SF", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "b4847bf2-1e29-4fa3-9f17-a3ae16c6c833", + "created_date": "2024-08-16 23:58:37.408000", + "last_modified_date": "2024-08-16 23:58:37.408000", + "version": 0, + "name": "Defensive Tackle", + "short_name": "DT", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "bcb2f1ee-d2e4-4131-a19f-4fc11a2c674b", + "created_date": "2024-08-16 23:58:37.386000", + "last_modified_date": "2024-08-16 23:58:37.386000", + "version": 0, + "name": "Offensive Tackle", + "short_name": "OF", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "c5f221ec-4ed7-4fd6-a761-6177ff3661c0", + "created_date": "2024-08-16 23:58:37.377000", + "last_modified_date": "2024-08-16 23:58:37.377000", + "version": 0, + "name": "Safety", + "short_name": "S", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "cd7e15d3-2d95-415b-9dfe-18ca3ed53a26", + "created_date": "2024-08-16 23:58:37.369000", + "last_modified_date": "2024-08-16 23:58:37.369000", + "version": 0, + "name": "Linebacker", + "short_name": "LB", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "cea8dc5a-e9c6-4f52-8644-3f3e1a4ddefe", + "created_date": "2024-08-16 23:58:37.416000", + "last_modified_date": "2024-08-16 23:58:37.416000", + "version": 0, + "name": "Pitcher", + "short_name": "P", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "cf03e12d-4c17-4165-8ede-d30107c4c4ae", + "created_date": "2024-08-16 23:58:37.418000", + "last_modified_date": "2024-08-16 23:58:37.418000", + "version": 0, + "name": "First Base", + "short_name": "1B", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "d113f005-7792-494d-ad98-b1ce9a30130c", + "created_date": "2024-08-16 23:58:37.359000", + "last_modified_date": "2024-08-16 23:58:37.359000", + "version": 0, + "name": "Running Back", + "short_name": "RB", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "d200e02a-5ae5-44a0-adab-cb11d07ad0e7", + "created_date": "2024-08-16 23:58:37.420000", + "last_modified_date": "2024-08-16 23:58:37.420000", + "version": 0, + "name": "Second Base", + "short_name": "2B", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "dc2034e1-eb61-4741-b16d-b1267ce21df4", + "created_date": "2024-08-16 23:58:37.368000", + "last_modified_date": "2024-08-16 23:58:37.368000", + "version": 0, + "name": "Defensive Line", + "short_name": "DL", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "dcf3cda6-3357-4878-ba9a-fe3e2b3b89e7", + "created_date": "2024-08-16 23:58:37.440000", + "last_modified_date": "2024-08-16 23:58:37.440000", + "version": 0, + "name": "Left Wing", + "short_name": "LW", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "e4201d7e-fe49-439e-ad2e-e95f27f05ed5", + "created_date": "2024-08-16 23:58:37.354000", + "last_modified_date": "2024-08-16 23:58:37.354000", + "version": 0, + "name": "Quarterback", + "short_name": "QB", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "eef704c2-2979-49cc-962f-cbd35574fbf9", + "created_date": "2024-08-16 23:58:37.409000", + "last_modified_date": "2024-08-16 23:58:37.409000", + "version": 0, + "name": "Nose Tackle", + "short_name": "NT", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "f01e852c-7e36-4365-9c68-2aa9c809a881", + "created_date": "2024-08-16 23:58:37.423000", + "last_modified_date": "2024-08-16 23:58:37.423000", + "version": 0, + "name": "Shortstop", + "short_name": "SS", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "f1d415bb-83dc-4fa4-baf3-a6102520b995", + "created_date": "2024-08-16 23:58:37.383000", + "last_modified_date": "2024-08-16 23:58:37.383000", + "version": 0, + "name": "Left Guard", + "short_name": "LG", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "f3fc447f-497f-4f98-b2ce-0f6e7141096a", + "created_date": "2024-08-16 23:58:37.372000", + "last_modified_date": "2024-08-16 23:58:37.372000", + "version": 0, + "name": "Defensive End", + "short_name": "DE", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + } + ], + "card_set": [ + { + "id": "0a2a52ab-93c1-49c2-b2f6-abf3b9c2c3ae", + "created_date": "2024-08-16 23:58:37.470000", + "last_modified_date": "2024-08-16 23:58:37.470000", + "version": 0, + "insert_set": 0, + "parallel_set": 1, + "name": "Pacific Copper", + "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" + }, + { + "id": "22bc331b-e604-46e5-9958-2e65489033d6", + "created_date": "2024-08-16 23:58:37.482000", + "last_modified_date": "2024-08-16 23:58:37.482000", + "version": 0, + "insert_set": 0, + "parallel_set": 0, + "name": "Finest Hour", + "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" + }, + { + "id": "61c2f801-1301-4e6e-8628-550f655a7f4d", + "created_date": "2024-08-16 23:58:37.481000", + "last_modified_date": "2024-08-16 23:58:37.481000", + "version": 0, + "insert_set": 0, + "parallel_set": 0, + "name": "Mystique", + "vendor_id": "d7617bf3-056a-489b-8563-628f2dd3da84" + }, + { + "id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", + "created_date": "2024-08-16 23:58:37.473000", + "last_modified_date": "2024-08-16 23:58:37.473000", + "version": 0, + "insert_set": 0, + "parallel_set": 0, + "name": "Pacific", + "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" + }, + { + "id": "6a1e1f44-28d8-4aaf-b8ee-9043115ae2ca", + "created_date": "2024-08-16 23:58:37.469000", + "last_modified_date": "2024-08-16 23:58:37.469000", + "version": 0, + "insert_set": 0, + "parallel_set": 1, + "name": "Mystique Gold", + "vendor_id": "d7617bf3-056a-489b-8563-628f2dd3da84" + }, + { + "id": "6f5bba5b-e3bd-41f7-9f37-3275057c31d2", + "created_date": "2024-08-16 23:58:37.479000", + "last_modified_date": "2024-08-16 23:58:37.479000", + "version": 0, + "insert_set": 0, + "parallel_set": 0, + "name": "Ultra", + "vendor_id": "d7617bf3-056a-489b-8563-628f2dd3da84" + }, + { + "id": "790e00ea-1434-45d0-86d2-9ba3beca5570", + "created_date": "2024-08-16 23:58:37.476000", + "last_modified_date": "2024-08-16 23:58:37.476000", + "version": 0, + "insert_set": 0, + "parallel_set": 0, + "name": "Bowman", + "vendor_id": "eb42fc0c-20ad-4e5b-b99a-552ac861db05" + }, + { + "id": "8cbb110c-9158-4ce4-a505-46e7af931f8f", + "created_date": "2024-08-16 23:58:37.465000", + "last_modified_date": "2024-08-16 23:58:37.465000", + "version": 0, + "insert_set": 1, + "parallel_set": 0, + "name": "Mystique Big Buzz", + "vendor_id": "d7617bf3-056a-489b-8563-628f2dd3da84" + }, + { + "id": "aa6b5458-5805-4858-807d-4e07e3138053", + "created_date": "2024-08-16 23:58:37.483000", + "last_modified_date": "2024-08-16 23:58:37.483000", + "version": 0, + "insert_set": 0, + "parallel_set": 0, + "name": "SP", + "vendor_id": "6472c0a1-2b4f-440f-813d-66ea3540f78c" + }, + { + "id": "b8a6e929-7dbd-439b-be79-4dd7f45d94fb", + "created_date": "2024-08-16 23:58:37.474000", + "last_modified_date": "2024-08-16 23:58:37.474000", + "version": 0, + "insert_set": 0, + "parallel_set": 0, + "name": "Fleer", + "vendor_id": "d7617bf3-056a-489b-8563-628f2dd3da84" + }, + { + "id": "bd416669-0d94-481c-96c4-6422b028fc9b", + "created_date": "2024-08-16 23:58:37.487000", + "last_modified_date": "2024-08-16 23:58:37.487000", + "version": 0, + "insert_set": 0, + "parallel_set": 0, + "name": "SP Authentic", + "vendor_id": "6472c0a1-2b4f-440f-813d-66ea3540f78c" + }, + { + "id": "ca49cae9-8597-4fce-91b9-fac6dd17bb63", + "created_date": "2024-08-16 23:58:37.485000", + "last_modified_date": "2024-08-16 23:58:37.485000", + "version": 0, + "insert_set": 0, + "parallel_set": 0, + "name": "SPX", + "vendor_id": "6472c0a1-2b4f-440f-813d-66ea3540f78c" + }, + { + "id": "cd4b5a4f-9200-4d12-acfd-006842d6b823", + "created_date": "2024-08-16 23:58:37.478000", + "last_modified_date": "2024-08-16 23:58:37.478000", + "version": 0, + "insert_set": 0, + "parallel_set": 0, + "name": "Leaf", + "vendor_id": "5e0f3f5d-c58b-4517-b30b-e767c34404ab" + }, + { + "id": "d7d83654-66c2-4c85-82f1-f10a7d1f3c7c", + "created_date": "2024-08-16 23:58:37.472000", + "last_modified_date": "2024-08-16 23:58:37.472000", + "version": 0, + "insert_set": 0, + "parallel_set": 1, + "name": "Pacific Gold", + "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" + }, + { + "id": "db3d6628-d789-4c24-822d-b5b9e1f8c0ab", + "created_date": "2024-08-16 23:58:37.488000", + "last_modified_date": "2024-08-16 23:58:37.488000", + "version": 0, + "insert_set": 0, + "parallel_set": 0, + "name": "Black Diamond", + "vendor_id": "6472c0a1-2b4f-440f-813d-66ea3540f78c" + } + ], + "team": [ + { + "id": "001a6867-9629-4314-ae56-78c2c267e572", + "created_date": "2024-08-16 23:58:37.272000", + "last_modified_date": "2024-08-16 23:58:37.272000", + "version": 0, + "name": "Chicago Bulls", + "short_name": "Bulls", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "035883e2-cc5c-40ae-9efb-9bdc0cc77199", + "created_date": "2024-08-16 23:58:37.266000", + "last_modified_date": "2024-08-16 23:58:37.266000", + "version": 0, + "name": "Philadelphia 76ers", + "short_name": "76ers", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "0839c619-4378-41dd-b5c2-3d398ee46870", + "created_date": "2024-08-16 23:58:37.348000", + "last_modified_date": "2024-08-16 23:58:37.348000", + "version": 0, + "name": "Phoenix Coyotes", + "short_name": "Coyotes", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "0a193e30-2e93-4747-a544-762eee9a9f12", + "created_date": "2024-08-16 23:58:37.310000", + "last_modified_date": "2024-08-16 23:58:37.310000", + "version": 0, + "name": "New Jersey Devils", + "short_name": "Devils", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "0c1d90c1-a0cb-4f58-94e6-58a419e4d8b8", + "created_date": "2024-08-16 23:58:37.173000", + "last_modified_date": "2024-08-16 23:58:37.173000", + "version": 0, + "name": "Cleveland Browns", + "short_name": "Browns", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "0ca47c2d-4f65-4c49-8882-8a2faa314b94", + "created_date": "2024-08-16 23:58:37.229000", + "last_modified_date": "2024-08-16 23:58:37.229000", + "version": 0, + "name": "Texas Rangers", + "short_name": "Rangers", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "117fbd2e-4fa5-4c09-bf19-10cfc59cff70", + "created_date": "2024-08-16 23:58:37.217000", + "last_modified_date": "2024-08-16 23:58:37.217000", + "version": 0, + "name": "Chicago White Sox", + "short_name": "White Sox", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "13c2ba23-a72a-49f4-9674-725c5b80a909", + "created_date": "2024-08-16 23:58:37.202000", + "last_modified_date": "2024-08-16 23:58:37.202000", + "version": 0, + "name": "Carolina Panthers", + "short_name": "Panthers", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "179f15e0-6628-40a0-b79d-30fffa5445b2", + "created_date": "2024-08-16 23:58:37.192000", + "last_modified_date": "2024-08-16 23:58:37.192000", + "version": 0, + "name": "Washington Redskins", + "short_name": "Redskins", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "1965b670-2125-4d3e-a71e-437895a14bd5", + "created_date": "2024-08-16 23:58:37.203000", + "last_modified_date": "2024-08-16 23:58:37.203000", + "version": 0, + "name": "New Orleans Saints", + "short_name": "Saints", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "1997c461-fd71-498a-ac29-590e4189c4ee", + "created_date": "2024-08-16 23:58:37.223000", + "last_modified_date": "2024-08-16 23:58:37.223000", + "version": 0, + "name": "Minnesota Twins", + "short_name": "Twins", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "1af4cb19-3507-4630-9eff-c97a81e422c2", + "created_date": "2024-08-16 23:58:37.177000", + "last_modified_date": "2024-08-16 23:58:37.177000", + "version": 0, + "name": "Tennessee Titans", + "short_name": "Titans", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "1cdf0d19-2ed8-471c-b689-cca82c557a5d", + "created_date": "2024-08-16 23:58:37.169000", + "last_modified_date": "2024-08-16 23:58:37.169000", + "version": 0, + "name": "Baltimore Ravens", + "short_name": "Ravens", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "1f184a5b-6448-4845-aa39-b84c4b566603", + "created_date": "2024-08-16 23:58:37.159000", + "last_modified_date": "2024-08-16 23:58:37.159000", + "version": 0, + "name": "Buffalo Bills", + "short_name": "Bills", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "217cc98f-be07-4b3c-8159-a0fd2b0957a0", + "created_date": "2024-08-16 23:58:37.268000", + "last_modified_date": "2024-08-16 23:58:37.268000", + "version": 0, + "name": "Washington Wizards", + "short_name": "Wizards", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "2394bfef-6a40-4430-9966-64cfc9a886ba", + "created_date": "2024-08-16 23:58:37.322000", + "last_modified_date": "2024-08-16 23:58:37.322000", + "version": 0, + "name": "Florida Panthers", + "short_name": "Panthers", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "25f52b52-2d3d-426a-8761-c24e5191bd8b", + "created_date": "2024-08-16 23:58:37.303000", + "last_modified_date": "2024-08-16 23:58:37.303000", + "version": 0, + "name": "Boston Bruins", + "short_name": "Bruins", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "2666f96d-7f3d-47cf-a901-3fde1dcde8b0", + "created_date": "2024-08-16 23:58:37.214000", + "last_modified_date": "2024-08-16 23:58:37.214000", + "version": 0, + "name": "Tampa Bay Devil Rays", + "short_name": "Devil Rays", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "29805313-e491-4263-ac98-5bfd009669ee", + "created_date": "2024-08-16 23:58:37.164000", + "last_modified_date": "2024-08-16 23:58:37.164000", + "version": 0, + "name": "Miami Dolphins", + "short_name": "Dolphins", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "2b7fd89f-f3dd-4a01-a494-c755cf7859fe", + "created_date": "2024-08-16 23:58:37.174000", + "last_modified_date": "2024-08-16 23:58:37.174000", + "version": 0, + "name": "Jacksonville Jaguars", + "short_name": "Jaguars", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "2bdf925d-f3ad-4692-aae1-d0bbba16c339", + "created_date": "2024-08-16 23:58:37.194000", + "last_modified_date": "2024-08-16 23:58:37.194000", + "version": 0, + "name": "Detroit Lions", + "short_name": "Lions", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "2c18d74c-892c-45e0-bd4c-b2c94aa5e0bc", + "created_date": "2024-08-16 23:58:37.255000", + "last_modified_date": "2024-08-16 23:58:37.255000", + "version": 0, + "name": "San Francisco Giants", + "short_name": "Giants", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "2c378b13-177a-4fef-aa09-688d8f501585", + "created_date": "2024-08-16 23:58:37.196000", + "last_modified_date": "2024-08-16 23:58:37.196000", + "version": 0, + "name": "Green Bay Packers", + "short_name": "Packers", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "2d890234-64dd-45ca-9155-cd1accc8afd1", + "created_date": "2024-08-16 23:58:37.280000", + "last_modified_date": "2024-08-16 23:58:37.280000", + "version": 0, + "name": "Toronto Raptors", + "short_name": "Raptors", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "2d9897fc-d80e-4eb9-aac9-db36b434bb43", + "created_date": "2024-08-16 23:58:37.186000", + "last_modified_date": "2024-08-16 23:58:37.186000", + "version": 0, + "name": "Arizona Cardinals", + "short_name": "Cardinals", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "2f0554e3-54c2-434f-b22a-f94c8dc32c23", + "created_date": "2024-08-16 23:58:37.279000", + "last_modified_date": "2024-08-16 23:58:37.279000", + "version": 0, + "name": "Milwaukee Bucks", + "short_name": "Bucks", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "323d2a5a-75c2-4636-9101-df353e25eaa4", + "created_date": "2024-08-16 23:58:37.304000", + "last_modified_date": "2024-08-16 23:58:37.304000", + "version": 0, + "name": "Buffalo Sabres", + "short_name": "Sabres", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "39ad73fd-1e5c-4f4f-9d17-32e01aaa7526", + "created_date": "2024-08-16 23:58:37.191000", + "last_modified_date": "2024-08-16 23:58:37.191000", + "version": 0, + "name": "Philadelphia Eagles", + "short_name": "Eagles", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "3d0e3c9c-7b41-4ed8-83ea-a45a037a6afb", + "created_date": "2024-08-16 23:58:37.188000", + "last_modified_date": "2024-08-16 23:58:37.188000", + "version": 0, + "name": "New York Giants", + "short_name": "Giants", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "3eb84f3e-fce5-4a06-8f60-e08107740ab8", + "created_date": "2024-08-16 23:58:37.172000", + "last_modified_date": "2024-08-16 23:58:37.172000", + "version": 0, + "name": "Cincinnati Bengals", + "short_name": "Bengals", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "3fec6af3-d87d-4dd5-b15c-1c6982135920", + "created_date": "2024-08-16 23:58:37.176000", + "last_modified_date": "2024-08-16 23:58:37.176000", + "version": 0, + "name": "Pittsburgh Steelers", + "short_name": "Steelers", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "4050759a-ece4-4c81-90a3-85d576eb4fbd", + "created_date": "2024-08-16 23:58:37.326000", + "last_modified_date": "2024-08-16 23:58:37.326000", + "version": 0, + "name": "Chicago Blackhawks", + "short_name": "Blackhawks", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "427383d4-203b-44b7-ad5e-6fc8026dce39", + "created_date": "2024-08-16 23:58:37.292000", + "last_modified_date": "2024-08-16 23:58:37.292000", + "version": 0, + "name": "Golden State Warriors", + "short_name": "Warriors", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "44158247-f016-4d95-8f1f-bd888b6ab4e8", + "created_date": "2024-08-16 23:58:37.243000", + "last_modified_date": "2024-08-16 23:58:37.243000", + "version": 0, + "name": "Pittsburgh Pirates", + "short_name": "Pirates", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "450677ba-37de-4255-a23d-0e9b896d4bd7", + "created_date": "2024-08-16 23:58:37.320000", + "last_modified_date": "2024-08-16 23:58:37.320000", + "version": 0, + "name": "Carolina Hurricanes", + "short_name": "Hurricanes", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "47bfdb12-7874-4245-bb85-1302359c494b", + "created_date": "2024-08-16 23:58:37.290000", + "last_modified_date": "2024-08-16 23:58:37.290000", + "version": 0, + "name": "Utah Jazz", + "short_name": "Jazz", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "48b372f7-b568-4f64-9f81-8cd770da7a8d", + "created_date": "2024-08-16 23:58:37.312000", + "last_modified_date": "2024-08-16 23:58:37.312000", + "version": 0, + "name": "New York Islanders", + "short_name": "Islanders", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "4a0a308b-2b0a-44c3-8d52-c90ffa660a1a", + "created_date": "2024-08-16 23:58:37.187000", + "last_modified_date": "2024-08-16 23:58:37.187000", + "version": 0, + "name": "Dallas Cowboys", + "short_name": "Cowboys", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "4c39f899-1653-406a-9780-e74b6266559a", + "created_date": "2024-08-16 23:58:37.227000", + "last_modified_date": "2024-08-16 23:58:37.227000", + "version": 0, + "name": "Seattle Mariners", + "short_name": "Mariners", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "4f4bdba0-ba5a-4f52-9b60-fd136c094aed", + "created_date": "2024-08-16 23:58:37.219000", + "last_modified_date": "2024-08-16 23:58:37.219000", + "version": 0, + "name": "Cleveland Indians", + "short_name": "Indians", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "4fecdfc3-4ee3-41ee-8352-9c6d64934105", + "created_date": "2024-08-16 23:58:37.262000", + "last_modified_date": "2024-08-16 23:58:37.262000", + "version": 0, + "name": "New York Knicks", + "short_name": "Knicks", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "5002d98a-6074-489a-9b47-452cb5451e7c", + "created_date": "2024-08-16 23:58:37.323000", + "last_modified_date": "2024-08-16 23:58:37.323000", + "version": 0, + "name": "Tampa Bay Lightnings", + "short_name": "Lightnings", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "50b4c394-0377-41f1-a976-13919fe3b608", + "created_date": "2024-08-16 23:58:37.256000", + "last_modified_date": "2024-08-16 23:58:37.256000", + "version": 0, + "name": "Boston Celtics", + "short_name": "Celtics", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "575a9788-621e-40f3-ad15-16750f545ab8", + "created_date": "2024-08-16 23:58:37.234000", + "last_modified_date": "2024-08-16 23:58:37.234000", + "version": 0, + "name": "New York Mets", + "short_name": "Mets", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "57ac5188-be46-4c20-87ba-bfaf24d9f9c1", + "created_date": "2024-08-16 23:58:37.331000", + "last_modified_date": "2024-08-16 23:58:37.331000", + "version": 0, + "name": "Nashville Predators", + "short_name": "Predators", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "5c8137a0-0483-4a34-9c37-5e59c7d34665", + "created_date": "2024-08-16 23:58:37.273000", + "last_modified_date": "2024-08-16 23:58:37.273000", + "version": 0, + "name": "Cleveland Cavaliers", + "short_name": "Cavaliers", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "5f753766-dff8-42ed-bf6d-ae05fd4353f4", + "created_date": "2024-08-16 23:58:37.212000", + "last_modified_date": "2024-08-16 23:58:37.212000", + "version": 0, + "name": "Boston Red Sox", + "short_name": "Red Sox", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "5f7fa954-cce0-4227-8ba4-689803583a30", + "created_date": "2024-08-16 23:58:37.283000", + "last_modified_date": "2024-08-16 23:58:37.283000", + "version": 0, + "name": "Denver Nuggets", + "short_name": "Nuggets", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "5f8d02c0-5c86-4ed7-99c2-c9ce113c73e2", + "created_date": "2024-08-16 23:58:37.308000", + "last_modified_date": "2024-08-16 23:58:37.308000", + "version": 0, + "name": "Toronto Maple Leafs", + "short_name": "Maple Leafs", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "6005c069-289d-4c07-8e5e-6cd77c76144a", + "created_date": "2024-08-16 23:58:37.341000", + "last_modified_date": "2024-08-16 23:58:37.341000", + "version": 0, + "name": "Anaheim Mighty Ducks", + "short_name": "Mighty Ducks", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "620bdeff-ff03-4837-b6df-ee5b9e4e8c71", + "created_date": "2024-08-16 23:58:37.213000", + "last_modified_date": "2024-08-16 23:58:37.213000", + "version": 0, + "name": "New York Yankees", + "short_name": "Yankees", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "6282602e-8c75-4e10-980e-fc296fced8ec", + "created_date": "2024-08-16 23:58:37.204000", + "last_modified_date": "2024-08-16 23:58:37.204000", + "version": 0, + "name": "St.Louis Rams", + "short_name": "Rams", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "63146d4c-bada-47ba-9d3e-bce28dd89baa", + "created_date": "2024-08-16 23:58:37.327000", + "last_modified_date": "2024-08-16 23:58:37.327000", + "version": 0, + "name": "Columbus Blue Jackets", + "short_name": "Blue Jackets", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "633c0ae8-979d-4a3f-a31a-0304a2552263", + "created_date": "2024-08-16 23:58:37.277000", + "last_modified_date": "2024-08-16 23:58:37.277000", + "version": 0, + "name": "Indiana Pacers", + "short_name": "Pacers", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "639578fd-9a05-4c7b-b0ad-6964939c3f50", + "created_date": "2024-08-16 23:58:37.345000", + "last_modified_date": "2024-08-16 23:58:37.345000", + "version": 0, + "name": "Dallas Stars", + "short_name": "Stars", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "63e7a35f-24e7-44bd-bebd-c8995a21da09", + "created_date": "2024-08-16 23:58:37.236000", + "last_modified_date": "2024-08-16 23:58:37.236000", + "version": 0, + "name": "Philadelphia Phillies", + "short_name": "Phillies", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "66ea5a89-f433-4192-9e90-7409e5581902", + "created_date": "2024-08-16 23:58:37.269000", + "last_modified_date": "2024-08-16 23:58:37.269000", + "version": 0, + "name": "Atlanta Hawks", + "short_name": "Hawks", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "6c081833-0558-4ec3-8311-5bc7aced52cc", + "created_date": "2024-08-16 23:58:37.264000", + "last_modified_date": "2024-08-16 23:58:37.264000", + "version": 0, + "name": "Orlando Magic", + "short_name": "Magic", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "6c318b96-214b-42ad-bec9-98cf2c02f190", + "created_date": "2024-08-16 23:58:37.333000", + "last_modified_date": "2024-08-16 23:58:37.333000", + "version": 0, + "name": "Calgary Flames", + "short_name": "Flames", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "6c6d740f-3f1c-49b2-b78d-7ec1b18a704c", + "created_date": "2024-08-16 23:58:37.316000", + "last_modified_date": "2024-08-16 23:58:37.316000", + "version": 0, + "name": "Pittsburgh Penguins", + "short_name": "Penguins", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "713f56be-1d53-426c-9c9d-77fe5c5c7368", + "created_date": "2024-08-16 23:58:37.334000", + "last_modified_date": "2024-08-16 23:58:37.334000", + "version": 0, + "name": "Colorado Avalanche", + "short_name": "Avalanche", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "760bea8c-530b-4a0f-a8d4-05e7b8311713", + "created_date": "2024-08-16 23:58:37.197000", + "last_modified_date": "2024-08-16 23:58:37.197000", + "version": 0, + "name": "Minnesota Vikings", + "short_name": "Vikings", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "76278aa5-7c0f-4963-a03f-c3d768fc45ad", + "created_date": "2024-08-16 23:58:37.210000", + "last_modified_date": "2024-08-16 23:58:37.210000", + "version": 0, + "name": "Baltimore Orioles", + "short_name": "Orioles", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "762d7f66-18d6-47f4-b59d-e42543304a4f", + "created_date": "2024-08-16 23:58:37.183000", + "last_modified_date": "2024-08-16 23:58:37.183000", + "version": 0, + "name": "San Diego Chargers", + "short_name": "Chargers", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "76968239-d7b7-4ff6-92aa-4587ff5511c6", + "created_date": "2024-08-16 23:58:37.247000", + "last_modified_date": "2024-08-16 23:58:37.247000", + "version": 0, + "name": "Arizona Diamondbacks", + "short_name": "Diamondbacks", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "7aa7ad8f-addf-4d5c-bb4d-d17dcb0ed421", + "created_date": "2024-08-16 23:58:37.287000", + "last_modified_date": "2024-08-16 23:58:37.287000", + "version": 0, + "name": "Minnesota Timberwolves", + "short_name": "Timberwolves", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "7b8f738e-0d8b-48c9-9f0a-033a05c339c2", + "created_date": "2024-08-16 23:58:37.250000", + "last_modified_date": "2024-08-16 23:58:37.250000", + "version": 0, + "name": "Los Angeles Dodgers", + "short_name": "Dodgers", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "7c7f779a-3ee4-430d-9881-471bd0038ed8", + "created_date": "2024-08-16 23:58:37.318000", + "last_modified_date": "2024-08-16 23:58:37.318000", + "version": 0, + "name": "Atlanta Trashers", + "short_name": "Trashers", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "7cb06db5-ab24-4339-be69-8a43a12d3a9d", + "created_date": "2024-08-16 23:58:37.205000", + "last_modified_date": "2024-08-16 23:58:37.205000", + "version": 0, + "name": "San Francisco 49ers", + "short_name": "49ers", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "7dbc78d3-b86a-4c99-af7f-4ebc9fb5ded8", + "created_date": "2024-08-16 23:58:37.249000", + "last_modified_date": "2024-08-16 23:58:37.249000", + "version": 0, + "name": "Colorado Rockies", + "short_name": "Rockies", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "83aa94d5-ec8d-477f-8ce2-b6c4c3bac2e5", + "created_date": "2024-08-16 23:58:37.180000", + "last_modified_date": "2024-08-16 23:58:37.180000", + "version": 0, + "name": "Kansas City Chiefs", + "short_name": "Chiefs", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "88301bde-3585-4d3d-830b-b4bd42b68dd5", + "created_date": "2024-08-16 23:58:37.208000", + "last_modified_date": "2024-08-16 23:58:37.208000", + "version": 0, + "name": "Houston Oilers", + "short_name": "Oilers", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "889741ce-2395-474b-a4eb-0a84495a3143", + "created_date": "2024-08-16 23:58:37.260000", + "last_modified_date": "2024-08-16 23:58:37.260000", + "version": 0, + "name": "New Jersey Nets", + "short_name": "Mets", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "88fb6005-0ec2-4e5e-8f78-8c2c172c0ce5", + "created_date": "2024-08-16 23:58:37.162000", + "last_modified_date": "2024-08-16 23:58:37.162000", + "version": 0, + "name": "Indianapolis Colts", + "short_name": "Colts", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "8bde8fc7-5f66-4c92-a00f-c5d2bcc67101", + "created_date": "2024-08-16 23:58:37.184000", + "last_modified_date": "2024-08-16 23:58:37.184000", + "version": 0, + "name": "Seattle Seahawks", + "short_name": "Seahawks", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "8d1ce4d1-58e7-45a5-b5bb-63b9d7f59842", + "created_date": "2024-08-16 23:58:37.233000", + "last_modified_date": "2024-08-16 23:58:37.233000", + "version": 0, + "name": "Montreal Expos", + "short_name": "Expos", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "8fd8e69b-eeb6-48d2-8043-812c01f1ec46", + "created_date": "2024-08-16 23:58:37.200000", + "last_modified_date": "2024-08-16 23:58:37.200000", + "version": 0, + "name": "Atlanta Falcons", + "short_name": "Falcons", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "934264a3-0333-44f3-a64a-8cb4b9a8e81e", + "created_date": "2024-08-16 23:58:37.166000", + "last_modified_date": "2024-08-16 23:58:37.166000", + "version": 0, + "name": "New England Patriots", + "short_name": "Patriots", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "93a667b8-85f4-45ae-a867-6df1d80e004f", + "created_date": "2024-08-16 23:58:37.314000", + "last_modified_date": "2024-08-16 23:58:37.314000", + "version": 0, + "name": "Philadelphia Flyers", + "short_name": "Flyers", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "963fb8ef-fb8f-4864-a249-1e00667c1915", + "created_date": "2024-08-16 23:58:37.286000", + "last_modified_date": "2024-08-16 23:58:37.286000", + "version": 0, + "name": "Houston Rockets", + "short_name": "Rockets", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "98f545c2-666b-449b-a072-e6146988cc19", + "created_date": "2024-08-16 23:58:37.306000", + "last_modified_date": "2024-08-16 23:58:37.306000", + "version": 0, + "name": "Montreal Canadiens", + "short_name": "Canadiens", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "99e0e365-347b-45cd-abf5-b37fbaffec8c", + "created_date": "2024-08-16 23:58:37.270000", + "last_modified_date": "2024-08-16 23:58:37.270000", + "version": 0, + "name": "Charlotte Hornets", + "short_name": "Hornets", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "9d318450-d458-490a-af79-68a4f8b001d4", + "created_date": "2024-08-16 23:58:37.296000", + "last_modified_date": "2024-08-16 23:58:37.296000", + "version": 0, + "name": "Los Angeles Lakers", + "short_name": "Lakers", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "9f2a4916-6ee3-4a44-addf-8de195b2a4ec", + "created_date": "2024-08-16 23:58:37.178000", + "last_modified_date": "2024-08-16 23:58:37.178000", + "version": 0, + "name": "Denver Broncos", + "short_name": "Broncos", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "a0128d73-e681-4b81-b7c3-64ee3fb538ac", + "created_date": "2024-08-16 23:58:37.220000", + "last_modified_date": "2024-08-16 23:58:37.220000", + "version": 0, + "name": "Detroit Tigers", + "short_name": "Tigers", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "a1740ba6-a74b-4481-990c-64af54375f15", + "created_date": "2024-08-16 23:58:37.230000", + "last_modified_date": "2024-08-16 23:58:37.230000", + "version": 0, + "name": "Atlanta Braves", + "short_name": "Braves", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "a188c637-a749-401f-9a93-20c96ce97d5d", + "created_date": "2024-08-16 23:58:37.297000", + "last_modified_date": "2024-08-16 23:58:37.297000", + "version": 0, + "name": "Phoenix Suns", + "short_name": "Suns", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "a4cc7d68-e9a4-465f-a877-ec69d351af62", + "created_date": "2024-08-16 23:58:37.339000", + "last_modified_date": "2024-08-16 23:58:37.339000", + "version": 0, + "name": "Vancouver Canucks", + "short_name": "Canucks", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "a9efdb53-2eb0-4e9a-8636-2da0cf0e595e", + "created_date": "2024-08-16 23:58:37.350000", + "last_modified_date": "2024-08-16 23:58:37.350000", + "version": 0, + "name": "San Jose Sharks", + "short_name": "Sharks", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "aaa8acb3-5d8a-4ff3-8774-9b691ca7984d", + "created_date": "2024-08-16 23:58:37.276000", + "last_modified_date": "2024-08-16 23:58:37.276000", + "version": 0, + "name": "Detroit Pistons", + "short_name": "Pistons", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "aae89396-6492-4e78-aa8d-d4e0f0f13b87", + "created_date": "2024-08-16 23:58:37.222000", + "last_modified_date": "2024-08-16 23:58:37.222000", + "version": 0, + "name": "Kansas City Royals", + "short_name": "Royals", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "ad9c54d8-edad-4620-828d-1f3a66825ef2", + "created_date": "2024-08-16 23:58:37.307000", + "last_modified_date": "2024-08-16 23:58:37.307000", + "version": 0, + "name": "Ottawa Senators", + "short_name": "Senators", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "b18050dd-a649-443d-b477-b8fe09cf8115", + "created_date": "2024-08-16 23:58:37.288000", + "last_modified_date": "2024-08-16 23:58:37.288000", + "version": 0, + "name": "San Antonio Spurs", + "short_name": "Spurs", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "b278553a-5a14-47cc-9de0-2a3a52487ac1", + "created_date": "2024-08-16 23:58:37.347000", + "last_modified_date": "2024-08-16 23:58:37.347000", + "version": 0, + "name": "Los Angeles Kings", + "short_name": "Kings", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "b336f3a1-be58-4962-8f6d-e035aa6ad423", + "created_date": "2024-08-16 23:58:37.238000", + "last_modified_date": "2024-08-16 23:58:37.238000", + "version": 0, + "name": "Cincinnati Reds", + "short_name": "Reds", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "b5fbcfbe-d644-41c6-a3fa-12b767a8fbe4", + "created_date": "2024-08-16 23:58:37.252000", + "last_modified_date": "2024-08-16 23:58:37.252000", + "version": 0, + "name": "San Diego Padres", + "short_name": "Padres", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "be316896-fd2d-4142-afb1-91c95a2d4268", + "created_date": "2024-08-16 23:58:37.298000", + "last_modified_date": "2024-08-16 23:58:37.298000", + "version": 0, + "name": "Portland Trail Blazers", + "short_name": "Blazers", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "becec8c2-aeeb-4d4a-9f8a-cae3a5ae14bd", + "created_date": "2024-08-16 23:58:37.237000", + "last_modified_date": "2024-08-16 23:58:37.237000", + "version": 0, + "name": "Chicago Cubs", + "short_name": "Cubs", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "bedd00af-d0d4-4b78-a0e7-f23c7b90bcb4", + "created_date": "2024-08-16 23:58:37.226000", + "last_modified_date": "2024-08-16 23:58:37.226000", + "version": 0, + "name": "Oakland Athletics", + "short_name": "Athletics", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "beebcc31-029d-488f-9748-91dd860d5d26", + "created_date": "2024-08-16 23:58:37.258000", + "last_modified_date": "2024-08-16 23:58:37.258000", + "version": 0, + "name": "Miami Heat", + "short_name": "Heat", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "bf9ab756-966f-4865-80f1-f5b1458dce51", + "created_date": "2024-08-16 23:58:37.325000", + "last_modified_date": "2024-08-16 23:58:37.325000", + "version": 0, + "name": "Washington Capitals", + "short_name": "Capitals", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "c0ae2c8a-46f2-4b9a-9319-6d3ba9a8e240", + "created_date": "2024-08-16 23:58:37.216000", + "last_modified_date": "2024-08-16 23:58:37.216000", + "version": 0, + "name": "Toronto Blue Jays", + "short_name": "Blue Jays", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "c12d24e5-01c3-4fbc-9594-d726ff4daa75", + "created_date": "2024-08-16 23:58:37.240000", + "last_modified_date": "2024-08-16 23:58:37.240000", + "version": 0, + "name": "Houston Astros", + "short_name": "Astros", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "c58f6aa5-3966-47a4-b9d5-f102854ee9da", + "created_date": "2024-08-16 23:58:37.242000", + "last_modified_date": "2024-08-16 23:58:37.242000", + "version": 0, + "name": "Milwaukee Brewers", + "short_name": "Brewers", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "c724eab7-d988-4c19-b973-425d65280514", + "created_date": "2024-08-16 23:58:37.337000", + "last_modified_date": "2024-08-16 23:58:37.337000", + "version": 0, + "name": "Minnesota Wild", + "short_name": "Wild", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "c80f077a-638b-4026-b956-9af57eed0541", + "created_date": "2024-08-16 23:58:37.313000", + "last_modified_date": "2024-08-16 23:58:37.313000", + "version": 0, + "name": "New York Rangers", + "short_name": "Rangers", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "d09dacce-a588-41bc-9c34-ca26755fcc7f", + "created_date": "2024-08-16 23:58:37.168000", + "last_modified_date": "2024-08-16 23:58:37.168000", + "version": 0, + "name": "New York Jets", + "short_name": "Jets", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "d2ca6ee6-a621-4516-80c2-59c2dd497554", + "created_date": "2024-08-16 23:58:37.207000", + "last_modified_date": "2024-08-16 23:58:37.207000", + "version": 0, + "name": "Houston Texans", + "short_name": "Texans", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "d2fdf923-086d-48bc-89e0-785e2c678b29", + "created_date": "2024-08-16 23:58:37.294000", + "last_modified_date": "2024-08-16 23:58:37.294000", + "version": 0, + "name": "Los Angeles Clippers", + "short_name": "Clippers", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "d98cc818-debe-427a-9bf1-20c3634b41d5", + "created_date": "2024-08-16 23:58:37.336000", + "last_modified_date": "2024-08-16 23:58:37.336000", + "version": 0, + "name": "Edmonton Oilers", + "short_name": "Oilers", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "dc14ff8b-6557-4d10-900d-56d5fb391389", + "created_date": "2024-08-16 23:58:37.246000", + "last_modified_date": "2024-08-16 23:58:37.246000", + "version": 0, + "name": "St.Louis Cardinals", + "short_name": "Cardinals", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "dd161125-04c7-4caa-ab0d-7d33007590b6", + "created_date": "2024-08-16 23:58:37.332000", + "last_modified_date": "2024-08-16 23:58:37.332000", + "version": 0, + "name": "St.Louis Blues", + "short_name": "Blues", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "e01349a3-56a5-4976-b66b-fca025deb3af", + "created_date": "2024-08-16 23:58:37.329000", + "last_modified_date": "2024-08-16 23:58:37.329000", + "version": 0, + "name": "Detroit Red Wings", + "short_name": "Red Wings", + "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" + }, + { + "id": "e02a4afe-b89d-4d4d-bb25-0c850f856841", + "created_date": "2024-08-16 23:58:37.224000", + "last_modified_date": "2024-08-16 23:58:37.224000", + "version": 0, + "name": "Anaheim Angels", + "short_name": "Angels", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "e355bf76-ec12-4804-b4d3-88c18542651c", + "created_date": "2024-08-16 23:58:37.198000", + "last_modified_date": "2024-08-16 23:58:37.198000", + "version": 0, + "name": "Tampa Bay Buccaneers", + "short_name": "Buccaneers", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "e644821c-2fd9-46fa-b5e5-b6fb50e1ac08", + "created_date": "2024-08-16 23:58:37.232000", + "last_modified_date": "2024-08-16 23:58:37.232000", + "version": 0, + "name": "Florida Marlins", + "short_name": "Marlins", + "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" + }, + { + "id": "e8cd9040-08de-4b5a-a631-5637ff10e093", + "created_date": "2024-08-16 23:58:37.282000", + "last_modified_date": "2024-08-16 23:58:37.282000", + "version": 0, + "name": "Dallas Mavericks", + "short_name": "Mavericks", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "ee83a64c-f2fa-4259-8876-8f6926d51879", + "created_date": "2024-08-16 23:58:37.193000", + "last_modified_date": "2024-08-16 23:58:37.193000", + "version": 0, + "name": "Chicago Bears", + "short_name": "Bears", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "f1b524e1-15df-4b69-9fe4-9b507734be74", + "created_date": "2024-08-16 23:58:37.291000", + "last_modified_date": "2024-08-16 23:58:37.291000", + "version": 0, + "name": "Vancouver Grizzlies", + "short_name": "Grizzlies", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "f6f36160-96ce-44bf-9021-ab8757d5b7d9", + "created_date": "2024-08-16 23:58:37.300000", + "last_modified_date": "2024-08-16 23:58:37.300000", + "version": 0, + "name": "Sacramento Kings", + "short_name": "Kings", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + }, + { + "id": "fa930af3-e772-49c6-90d0-901d9778faa8", + "created_date": "2024-08-16 23:58:37.182000", + "last_modified_date": "2024-08-16 23:58:37.182000", + "version": 0, + "name": "Oakland Raiders", + "short_name": "Raiders", + "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" + }, + { + "id": "fd85a9b2-5ef3-4eb1-8370-37c286974971", + "created_date": "2024-08-16 23:58:37.301000", + "last_modified_date": "2024-08-16 23:58:37.301000", + "version": 0, + "name": "Seattle SuperSonics", + "short_name": "SuperSonics", + "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" + } + ], + "comic_work": [ + { + "id": "3aeb71e9-0a0b-43d0-bdfe-2244f0e6ce64", + "created_date": "2024-08-16 23:58:35.423000", + "last_modified_date": "2024-08-16 23:58:35.423000", + "version": 0, + "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", + "comic_id": "e54ebebe-701a-4d60-88ce-4df9b34da6ca", + "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" + }, + { + "id": "41f678ef-7da7-496e-8a5f-d197bddd06c5", + "created_date": "2024-08-16 23:58:35.419000", + "last_modified_date": "2024-08-16 23:58:35.419000", + "version": 0, + "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", + "comic_id": "b6383b7f-1c86-42d9-a3f6-d2a4bc96dc51", + "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" + }, + { + "id": "5412b81f-66e3-4539-82a6-cf9bc2665367", + "created_date": "2024-08-16 23:58:35.416000", + "last_modified_date": "2024-08-16 23:58:35.416000", + "version": 0, + "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", + "comic_id": "34baf0ec-2af4-4ceb-9f54-ab92fb9a0b96", + "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" + }, + { + "id": "56ce28ea-48e9-454a-872d-f1542206face", + "created_date": "2024-08-16 23:58:35.430000", + "last_modified_date": "2024-08-16 23:58:35.430000", + "version": 0, + "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", + "comic_id": "46e41bf6-1c75-4e2b-9905-9dfda3bcfa9c", + "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" + }, + { + "id": "5cb922e2-7304-4f55-a3cb-9f9db34bffbc", + "created_date": "2024-08-16 23:58:35.438000", + "last_modified_date": "2024-08-16 23:58:35.438000", + "version": 0, + "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", + "comic_id": "dea3ab08-5e3e-4438-b28f-fcb711a3b593", + "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" + }, + { + "id": "65343fb2-2217-4815-8352-fff6dab1eab6", + "created_date": "2024-08-16 23:58:35.421000", + "last_modified_date": "2024-08-16 23:58:35.421000", + "version": 0, + "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", + "comic_id": "d37740d1-9c0d-480f-bf14-16f868f50a2c", + "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" + }, + { + "id": "75321376-d97d-4c73-be91-157f3a0bc2e1", + "created_date": "2024-08-16 23:58:35.452000", + "last_modified_date": "2024-08-16 23:58:35.452000", + "version": 0, + "artist_id": "b81c0868-a38c-4a2c-a64e-92a17c88fa72", + "comic_id": "b6383b7f-1c86-42d9-a3f6-d2a4bc96dc51", + "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" + }, + { + "id": "9df312f5-fe0c-4379-976c-b864c1df1efd", + "created_date": "2024-08-16 23:58:35.435000", + "last_modified_date": "2024-08-16 23:58:35.435000", + "version": 0, + "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", + "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", + "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" + }, + { + "id": "b2f40e1f-4c4d-4f42-b470-9c11bdce75e2", + "created_date": "2024-08-16 23:58:35.414000", + "last_modified_date": "2024-08-16 23:58:35.414000", + "version": 0, + "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", + "comic_id": "5aa143a9-d0a1-457f-b178-c8c71951dd91", + "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" + }, + { + "id": "b36a59f1-e82f-426d-916b-05e8e7a2893a", + "created_date": "2024-08-16 23:58:35.412000", + "last_modified_date": "2024-08-16 23:58:35.412000", + "version": 0, + "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", + "comic_id": "03c5b145-69d4-4d7e-8323-cb2f81060829", + "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" + }, + { + "id": "b66f93bf-0e33-4837-bc1c-9176bcee32e9", + "created_date": "2024-08-16 23:58:35.428000", + "last_modified_date": "2024-08-16 23:58:35.428000", + "version": 0, + "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", + "comic_id": "8ad36a79-9436-455d-8096-0f1b73c22f13", + "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" + }, + { + "id": "b9015fe2-25f0-4a13-a476-98d8065d2167", + "created_date": "2024-08-16 23:58:35.446000", + "last_modified_date": "2024-08-16 23:58:35.446000", + "version": 0, + "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", + "comic_id": "becf411e-8fe2-470e-8bde-7991c59988e0", + "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" + }, + { + "id": "bba7fe64-6f8a-4cfa-acdf-9430be20a5dc", + "created_date": "2024-08-16 23:58:35.444000", + "last_modified_date": "2024-08-16 23:58:35.444000", + "version": 0, + "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", + "comic_id": "1b7de491-6bbb-404e-a2e5-a20a123e3fe5", + "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" + }, + { + "id": "c0bcd7d4-2afd-41e4-afba-52eaee345f02", + "created_date": "2024-08-16 23:58:35.425000", + "last_modified_date": "2024-08-16 23:58:35.425000", + "version": 0, + "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", + "comic_id": "fd313197-70f1-45d5-8ca3-c8b6d828254e", + "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" + }, + { + "id": "c7a46f94-1f47-45c9-ad03-ea69518023f7", + "created_date": "2024-08-16 23:58:35.407000", + "last_modified_date": "2024-08-16 23:58:35.407000", + "version": 0, + "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", + "comic_id": "54ce47f2-d611-4d22-9ef5-c57e6d3e5967", + "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" + }, + { + "id": "d403a597-efb1-4b6b-93ba-432aeb6464f7", + "created_date": "2024-08-16 23:58:35.450000", + "last_modified_date": "2024-08-16 23:58:35.450000", + "version": 0, + "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", + "comic_id": "e6b93088-b350-412b-9634-227216ff252a", + "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" + }, + { + "id": "dffde7c9-fb4b-4d8c-a568-51fb4d0e33be", + "created_date": "2024-08-16 23:58:35.433000", + "last_modified_date": "2024-08-16 23:58:35.433000", + "version": 0, + "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", + "comic_id": "abfcb11c-7757-4db8-879e-b0d1803819d9", + "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" + }, + { + "id": "f7d36434-f17b-494c-ab13-0cf3fffef6f3", + "created_date": "2024-08-16 23:58:35.441000", + "last_modified_date": "2024-08-16 23:58:35.441000", + "version": 0, + "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", + "comic_id": "240a8a5d-eb07-4ce1-9fdd-a1b888c7426c", + "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" + } + ], + "worktype": [ + { + "id": "6e454a13-672e-40fc-98ec-82e156fbf2e7", + "created_date": "2024-08-16 23:58:34.802000", + "last_modified_date": "2024-08-16 23:58:34.802000", + "version": 0, + "name": "Penciler" + }, + { + "id": "8bca067a-7ba7-4f94-b317-c521b315bee8", + "created_date": "2024-08-16 23:58:34.797000", + "last_modified_date": "2024-08-16 23:58:34.797000", + "version": 0, + "name": "Writer" + }, + { + "id": "d506f76b-ce8d-4789-9716-64cd0c10ebc6", + "created_date": "2024-08-16 23:58:34.807000", + "last_modified_date": "2024-08-16 23:58:34.807000", + "version": 0, + "name": "Inker" + } + ], + "artist": [ + { + "id": "7d1e0a4c-eb44-4208-962c-7c069a81c3ac", + "created_date": "2024-08-16 23:58:34.787000", + "last_modified_date": "2024-08-16 23:58:34.787000", + "version": 0, + "name": "Whedon, Joss" + }, + { + "id": "b457d79a-3a04-41fa-b135-f6f86af575af", + "created_date": "2024-08-16 23:58:34.780000", + "last_modified_date": "2024-08-16 23:58:34.780000", + "version": 0, + "name": "Turner, Michael" + }, + { + "id": "b7d3316c-5368-4554-9857-a1b96231d751", + "created_date": "2024-08-16 23:58:34.784000", + "last_modified_date": "2024-08-16 23:58:34.784000", + "version": 0, + "name": "Marz, Ron" + }, + { + "id": "b81c0868-a38c-4a2c-a64e-92a17c88fa72", + "created_date": "2024-08-16 23:58:34.793000", + "last_modified_date": "2024-08-16 23:58:34.793000", + "version": 0, + "name": "Bendis, Brian Michael" + }, + { + "id": "d5a3a6de-825f-46f5-90d4-df6482ba9768", + "created_date": "2024-08-16 23:58:34.790000", + "last_modified_date": "2024-08-16 23:58:34.790000", + "version": 0, + "name": "Land, Greg" + } + ], + "module_data": [ + { + "id": "2589c9b6-45ae-4ef7-9fd4-de8a903c4576", + "created_date": "2024-08-16 23:58:37.138000", + "last_modified_date": "2024-08-16 23:58:37.138000", + "version": 0, + "module_name": "Comics", + "import_data": 0 + }, + { + "id": "78e458f5-3d26-41b0-871c-9a342613f6fb", + "created_date": "2024-08-16 23:58:37.143000", + "last_modified_date": "2024-08-16 23:58:37.143000", + "version": 0, + "module_name": "Media", + "import_data": 0 + }, + { + "id": "d43897dc-6ea9-4777-ba62-41e947a30983", + "created_date": "2024-08-16 23:58:34.698000", + "last_modified_date": "2024-08-16 23:58:34.698000", + "version": 0, + "module_name": "Bookshelf", + "import_data": 0 + }, + { + "id": "dd46b95f-e2cb-439c-b4d7-a109fec94cd7", + "created_date": "2024-08-16 23:58:37.607000", + "last_modified_date": "2024-08-16 23:58:37.607000", + "version": 0, + "module_name": "TradeYourSportsCards", + "import_data": 0 + } + ], + "bookshelf_publisher": [], + "player": [ + { + "id": "0166756b-e546-42fa-bdc9-dca8bd88278d", + "created_date": "2024-08-16 23:58:37.540000", + "last_modified_date": "2024-08-16 23:58:37.540000", + "version": 0, + "first_name": "Antowain", + "last_name": "Smith" + }, + { + "id": "0204701a-8421-418c-bfa4-90c5de616108", + "created_date": "2024-08-16 23:58:37.517000", + "last_modified_date": "2024-08-16 23:58:37.517000", + "version": 0, + "first_name": "Troy", + "last_name": "Aikman" + }, + { + "id": "02fc65f6-7d12-41c1-936d-e5dd20da58ba", + "created_date": "2024-08-16 23:58:37.504000", + "last_modified_date": "2024-08-16 23:58:37.504000", + "version": 0, + "first_name": "Jerome", + "last_name": "Bettis" + }, + { + "id": "0fa94659-7cfe-41ac-abec-ed00462f60dd", + "created_date": "2024-08-16 23:58:37.536000", + "last_modified_date": "2024-08-16 23:58:37.536000", + "version": 0, + "first_name": "Charlie", + "last_name": "Garner" + }, + { + "id": "12bd126b-3c53-493f-a2d6-da68f6f212ea", + "created_date": "2024-08-16 23:58:37.508000", + "last_modified_date": "2024-08-16 23:58:37.508000", + "version": 0, + "first_name": "Kevin", + "last_name": "Lockett" + }, + { + "id": "1560dc39-f3f9-43e1-82d9-a9fb0dab0793", + "created_date": "2024-08-16 23:58:37.537000", + "last_modified_date": "2024-08-16 23:58:37.537000", + "version": 0, + "first_name": "Drew", + "last_name": "Bledsoe" + }, + { + "id": "23710a7d-27f6-4017-8377-d4eec2ab9f45", + "created_date": "2024-08-16 23:58:37.525000", + "last_modified_date": "2024-08-16 23:58:37.525000", + "version": 0, + "first_name": "Torrance", + "last_name": "Small" + }, + { + "id": "24634014-f395-4409-aeb3-fa0af013b302", + "created_date": "2024-08-16 23:58:37.541000", + "last_modified_date": "2024-08-16 23:58:37.541000", + "version": 0, + "first_name": "Terry", + "last_name": "Glenn" + }, + { + "id": "2547e730-6a1d-418d-98c1-4d1c09c9f021", + "created_date": "2024-08-16 23:58:37.511000", + "last_modified_date": "2024-08-16 23:58:37.511000", + "version": 0, + "first_name": "James", + "last_name": "Jett" + }, + { + "id": "2c2b9866-eae7-43b3-b0af-6a56a4dec2d6", + "created_date": "2024-08-16 23:58:37.530000", + "last_modified_date": "2024-08-16 23:58:37.530000", + "version": 0, + "first_name": "Chris", + "last_name": "Chandler" + }, + { + "id": "4172d642-5b3d-4c60-8987-e3d37aee1822", + "created_date": "2024-08-16 23:58:37.507000", + "last_modified_date": "2024-08-16 23:58:37.507000", + "version": 0, + "first_name": "Warren", + "last_name": "Moon" + }, + { + "id": "4216df43-16a4-4e94-a2c8-324f5785e2b9", + "created_date": "2024-08-16 23:58:37.542000", + "last_modified_date": "2024-08-16 23:58:37.542000", + "version": 0, + "first_name": "Jerry", + "last_name": "Rice" + }, + { + "id": "48afc549-12a8-46ce-a5d2-c01d8dca4492", + "created_date": "2024-08-16 23:58:37.535000", + "last_modified_date": "2024-08-16 23:58:37.535000", + "version": 0, + "first_name": "Tai", + "last_name": "Streets" + }, + { + "id": "4f6c6a79-9512-4927-8be1-3c74405870e1", + "created_date": "2024-08-16 23:58:37.528000", + "last_modified_date": "2024-08-16 23:58:37.528000", + "version": 0, + "first_name": "Adrian", + "last_name": "Murrell" + }, + { + "id": "5038dd4f-203f-4cb7-99dd-c12c7da4735f", + "created_date": "2024-08-16 23:58:37.498000", + "last_modified_date": "2024-08-16 23:58:37.498000", + "version": 0, + "first_name": "Jamal", + "last_name": "Lewis" + }, + { + "id": "5ce64865-db01-4c55-9bb1-72ea1bea9b08", + "created_date": "2024-08-16 23:58:37.499000", + "last_modified_date": "2024-08-16 23:58:37.499000", + "version": 0, + "first_name": "Jermaine", + "last_name": "Lewis" + }, + { + "id": "5ee26469-7618-45ef-ba98-f83c3931f2d3", + "created_date": "2024-08-16 23:58:37.495000", + "last_modified_date": "2024-08-16 23:58:37.495000", + "version": 0, + "first_name": "Tim", + "last_name": "Couch" + }, + { + "id": "61baa0c9-4766-4f95-b3b5-dbb2eee80971", + "created_date": "2024-08-16 23:58:37.522000", + "last_modified_date": "2024-08-16 23:58:37.522000", + "version": 0, + "first_name": "Ron", + "last_name": "Dayne" + }, + { + "id": "6be96b96-6d3e-4106-8078-3b5d8c2d79da", + "created_date": "2024-08-16 23:58:37.532000", + "last_modified_date": "2024-08-16 23:58:37.532000", + "version": 0, + "first_name": "Ricky", + "last_name": "Williams" + }, + { + "id": "7b76a40c-c01c-4050-b4fb-c9da33fa1bdd", + "created_date": "2024-08-16 23:58:37.520000", + "last_modified_date": "2024-08-16 23:58:37.520000", + "version": 0, + "first_name": "Chris", + "last_name": "Brazzell" + }, + { + "id": "80ad0a60-a164-4d6c-9d94-155eaafd71f1", + "created_date": "2024-08-16 23:58:37.527000", + "last_modified_date": "2024-08-16 23:58:37.527000", + "version": 0, + "first_name": "Chad", + "last_name": "Lewis" + }, + { + "id": "87ddf101-82b7-4c17-9789-945c1696c1a3", + "created_date": "2024-08-16 23:58:37.531000", + "last_modified_date": "2024-08-16 23:58:37.531000", + "version": 0, + "first_name": "Danny", + "last_name": "Kanell" + }, + { + "id": "887504f8-5038-4335-90ba-487ac139155c", + "created_date": "2024-08-16 23:58:37.497000", + "last_modified_date": "2024-08-16 23:58:37.497000", + "version": 0, + "first_name": "Aaron", + "last_name": "Shea" + }, + { + "id": "8b8c84d1-244f-4d2b-a1e4-b3ff3f57cadd", + "created_date": "2024-08-16 23:58:37.523000", + "last_modified_date": "2024-08-16 23:58:37.523000", + "version": 0, + "first_name": "Na", + "last_name": "Brown" + }, + { + "id": "91a1a8b2-5dc8-4cbe-857e-25fd53ffc44e", + "created_date": "2024-08-16 23:58:37.512000", + "last_modified_date": "2024-08-16 23:58:37.512000", + "version": 0, + "first_name": "Mack", + "last_name": "Strong" + }, + { + "id": "983cc139-f2a2-46cd-9fdc-3ec1abf0d5a5", + "created_date": "2024-08-16 23:58:37.505000", + "last_modified_date": "2024-08-16 23:58:37.505000", + "version": 0, + "first_name": "Kordell", + "last_name": "Stewart" + }, + { + "id": "9cfea3d3-dc20-4cb0-a7c5-b12abb7f2520", + "created_date": "2024-08-16 23:58:37.515000", + "last_modified_date": "2024-08-16 23:58:37.515000", + "version": 0, + "first_name": "Brock", + "last_name": "Huard" + }, + { + "id": "a189b76f-9390-49f1-b5ab-47b35d00cb7f", + "created_date": "2024-08-16 23:58:37.493000", + "last_modified_date": "2024-08-16 23:58:37.493000", + "version": 0, + "first_name": "Tedy", + "last_name": "Bruschi" + }, + { + "id": "ac6b69ff-970a-4ead-bc5e-73dbcb5a4040", + "created_date": "2024-08-16 23:58:37.533000", + "last_modified_date": "2024-08-16 23:58:37.533000", + "version": 0, + "first_name": "Jeff", + "last_name": "Garcia" + }, + { + "id": "c9573462-268d-4138-9a33-1551e4e61f26", + "created_date": "2024-08-16 23:58:37.509000", + "last_modified_date": "2024-08-16 23:58:37.509000", + "version": 0, + "first_name": "Rich", + "last_name": "Gannon" + }, + { + "id": "cd4e3f5f-1e03-4d9b-973a-e8f9b7892dd8", + "created_date": "2024-08-16 23:58:37.503000", + "last_modified_date": "2024-08-16 23:58:37.503000", + "version": 0, + "first_name": "Chris", + "last_name": "Fuamatu-Ma'afala" + }, + { + "id": "d6b1a09d-3984-41cb-8b0a-d37d77e2c8ff", + "created_date": "2024-08-16 23:58:37.490000", + "last_modified_date": "2024-08-16 23:58:37.490000", + "version": 0, + "first_name": "Jerome", + "last_name": "Pathon" + }, + { + "id": "d8b7a3fd-8944-4a0a-b1d9-d42a33314f24", + "created_date": "2024-08-16 23:58:37.544000", + "last_modified_date": "2024-08-16 23:58:37.544000", + "version": 0, + "first_name": "Terrell", + "last_name": "Owens" + }, + { + "id": "de1e8d59-c2e2-4f68-a72a-14b4322fdf88", + "created_date": "2024-08-16 23:58:37.545000", + "last_modified_date": "2024-08-16 23:58:37.545000", + "version": 0, + "first_name": "Isaac", + "last_name": "Bruce" + }, + { + "id": "e3cb8252-3112-4d34-b20c-e586d900d17e", + "created_date": "2024-08-16 23:58:37.516000", + "last_modified_date": "2024-08-16 23:58:37.516000", + "version": 0, + "first_name": "Ricky", + "last_name": "Watters" + }, + { + "id": "e83cddec-19b0-4d35-929c-8acd7a7b7cbf", + "created_date": "2024-08-16 23:58:37.501000", + "last_modified_date": "2024-08-16 23:58:37.501000", + "version": 0, + "first_name": "Tony", + "last_name": "Banks" + }, + { + "id": "ec53c149-f00d-4130-8cf5-a65102a70ca2", + "created_date": "2024-08-16 23:58:37.546000", + "last_modified_date": "2024-08-16 23:58:37.546000", + "version": 0, + "first_name": "Trung", + "last_name": "Canidate" + }, + { + "id": "febb0d11-ac0f-49ea-b92d-14b44128eb37", + "created_date": "2024-08-16 23:58:37.519000", + "last_modified_date": "2024-08-16 23:58:37.519000", + "version": 0, + "first_name": "David", + "last_name": "LaFleur" + } + ], + "mail_account": [ + { + "id": "bfb66ce0-932d-413d-a996-923373a8ed49", + "created_date": "2024-08-16 23:58:34.684000", + "last_modified_date": "2024-08-16 23:58:34.684000", + "version": 0, + "host": "corky.svpdata.eu", + "port": 143, + "protocol": "imap", + "user_name": "thomas.peetz@thpeetz.de", + "password": "fS9f4JYDIO7A", + "start_tls": 1 + } + ], + "token": [], + "story_arc": [ + { + "id": "26a2f57e-a3f8-4ee4-8961-d49787af94c7", + "created_date": "2024-08-16 23:58:35.466000", + "last_modified_date": "2024-08-16 23:58:35.466000", + "version": 0, + "name": "Bloom", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb" + }, + { + "id": "6c4b5e81-d951-4f7b-9a56-a862f8150434", + "created_date": "2024-08-16 23:58:35.457000", + "last_modified_date": "2024-08-16 23:58:35.457000", + "version": 0, + "name": "Higher Learning", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb" + }, + { + "id": "fa78f350-91c4-4c57-9183-9293cdef6eec", + "created_date": "2024-08-16 23:58:35.461000", + "last_modified_date": "2024-08-16 23:58:35.461000", + "version": 0, + "name": "Mind Games", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb" + } + ], + "issue": [ + { + "id": "009dc046-a9e4-4bd6-8a8a-ffe34d54d8a1", + "created_date": "2024-08-16 23:58:36.128000", + "last_modified_date": "2024-08-16 23:58:36.128000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "f3231681-cd2b-4ff9-bfa2-5d8f631bee4d", + "volume_id": null + }, + { + "id": "01272372-57d4-4b94-8d95-1d09c5eb18d2", + "created_date": "2024-08-16 23:58:36.181000", + "last_modified_date": "2024-08-16 23:58:36.181000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", + "volume_id": null + }, + { + "id": "0138df17-570c-4072-9c8e-cf9cbf7b30e3", + "created_date": "2024-08-16 23:58:36.050000", + "last_modified_date": "2024-08-16 23:58:36.050000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", + "volume_id": null + }, + { + "id": "016d7c3a-522c-410e-a440-77e483f7f7d1", + "created_date": "2024-08-16 23:58:36.610000", + "last_modified_date": "2024-08-16 23:58:36.610000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "27", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "01bbd3dd-ef86-492c-8208-bb5c2aa5d984", + "created_date": "2024-08-16 23:58:36.131000", + "last_modified_date": "2024-08-16 23:58:36.131000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "c0543c9f-d712-4dce-9b59-2bf73b800b31", + "volume_id": null + }, + { + "id": "01c7e2a2-df2a-4244-8165-75458ce62401", + "created_date": "2024-08-16 23:58:36.026000", + "last_modified_date": "2024-08-16 23:58:36.026000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "16", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", + "volume_id": null + }, + { + "id": "01f83a75-adbe-485b-a726-16e4d1d5b691", + "created_date": "2024-08-16 23:58:36.943000", + "last_modified_date": "2024-08-16 23:58:36.943000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "43", + "comic_id": "2a4b287e-4b05-4016-8eb4-21fc225be24d", + "volume_id": null + }, + { + "id": "023a721d-1951-432c-96b6-c8e6bf5870fc", + "created_date": "2024-08-16 23:58:36.880000", + "last_modified_date": "2024-08-16 23:58:36.880000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", + "volume_id": null + }, + { + "id": "0283c944-0c5b-4a50-bf53-a11e6bdd1280", + "created_date": "2024-08-16 23:58:35.616000", + "last_modified_date": "2024-08-16 23:58:35.616000", + "version": 0, + "in_stock": 0, + "is_read": 1, + "issue_number": "3", + "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", + "volume_id": null + }, + { + "id": "02f6f28e-3672-4893-bc29-11fc3c637ff3", + "created_date": "2024-08-16 23:58:36.344000", + "last_modified_date": "2024-08-16 23:58:36.344000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "8", + "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", + "volume_id": null + }, + { + "id": "035fa8c9-2107-4bdf-90d7-b6fede18aeb1", + "created_date": "2024-08-16 23:58:36.871000", + "last_modified_date": "2024-08-16 23:58:36.871000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", + "volume_id": null + }, + { + "id": "03d219d7-0faf-4c2d-9b78-2113ccdfeca2", + "created_date": "2024-08-16 23:58:36.606000", + "last_modified_date": "2024-08-16 23:58:36.606000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "24", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "04069b6d-aac0-4ff0-bb22-68c6483f775e", + "created_date": "2024-08-16 23:58:37.100000", + "last_modified_date": "2024-08-16 23:58:37.100000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "07574acf-8f77-4da7-87fd-c49f40536baf", + "volume_id": null + }, + { + "id": "04384bcf-1806-402a-999d-356d512625f3", + "created_date": "2024-08-16 23:58:35.803000", + "last_modified_date": "2024-08-16 23:58:35.803000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "b51c738d-8ba1-4451-8f89-f49c1968ac42", + "volume_id": null + }, + { + "id": "04f41e26-37ac-4264-aa9c-8bbb46de41ff", + "created_date": "2024-08-16 23:58:36.264000", + "last_modified_date": "2024-08-16 23:58:36.264000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "5d0fd720-7875-4f9f-86eb-07ee0723908f", + "volume_id": null + }, + { + "id": "0600fbe7-a8d2-4373-8c44-b266d42eafa2", + "created_date": "2024-08-16 23:58:36.227000", + "last_modified_date": "2024-08-16 23:58:36.227000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "fe7fb34e-5ff2-4f6d-a649-0be19a368f46", + "volume_id": null + }, + { + "id": "06888bbc-1c0e-4a99-b976-e60d5b2b0dcc", + "created_date": "2024-08-16 23:58:35.819000", + "last_modified_date": "2024-08-16 23:58:35.819000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "89c5ea13-997a-4831-87cc-ee76ea05c71e", + "volume_id": null + }, + { + "id": "06e5bc77-7b77-4bda-adba-2f4d2781b89f", + "created_date": "2024-08-16 23:58:37.022000", + "last_modified_date": "2024-08-16 23:58:37.022000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "514", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "070d66c0-ac95-44e2-ab97-e0221308c027", + "created_date": "2024-08-16 23:58:36.494000", + "last_modified_date": "2024-08-16 23:58:36.494000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "8", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "072689aa-ebf2-4b1d-9283-0217c1e55ea8", + "created_date": "2024-08-16 23:58:36.117000", + "last_modified_date": "2024-08-16 23:58:36.117000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "17b50be7-aca7-446e-8729-3706f636d29d", + "volume_id": null + }, + { + "id": "076704a2-3263-4b71-a081-f5588a9fec1b", + "created_date": "2024-08-16 23:58:35.789000", + "last_modified_date": "2024-08-16 23:58:35.789000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "5a72a63d-8fbd-46a8-b201-9b6e035c782a", + "volume_id": null + }, + { + "id": "087d31b2-5c22-43f1-8fa5-fa712f5f5ba0", + "created_date": "2024-08-16 23:58:36.199000", + "last_modified_date": "2024-08-16 23:58:36.199000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", + "volume_id": null + }, + { + "id": "08c7d893-537d-4e4c-83ad-214e70dd2ab5", + "created_date": "2024-08-16 23:58:36.414000", + "last_modified_date": "2024-08-16 23:58:36.414000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "50", + "comic_id": "57965b27-1330-4921-8c0b-4b09ee06084f", + "volume_id": null + }, + { + "id": "08f61e9c-7c03-459e-b9d3-643f8e6f63d6", + "created_date": "2024-08-16 23:58:36.620000", + "last_modified_date": "2024-08-16 23:58:36.620000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "33", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "095efe2d-ce27-4614-a731-ea1aa760dce9", + "created_date": "2024-08-16 23:58:36.225000", + "last_modified_date": "2024-08-16 23:58:36.225000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "fe7fb34e-5ff2-4f6d-a649-0be19a368f46", + "volume_id": null + }, + { + "id": "09aa99f6-457d-47fd-81df-7aa729888075", + "created_date": "2024-08-16 23:58:37.004000", + "last_modified_date": "2024-08-16 23:58:37.004000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "851bc748-2602-4f20-826f-a59a7087d11f", + "volume_id": null + }, + { + "id": "09b4f24c-e6cf-4aa4-802e-4d21bed00b1f", + "created_date": "2024-08-16 23:58:36.205000", + "last_modified_date": "2024-08-16 23:58:36.205000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", + "volume_id": null + }, + { + "id": "0a347f5f-293a-4fd6-a927-7504fd815bc2", + "created_date": "2024-08-16 23:58:36.492000", + "last_modified_date": "2024-08-16 23:58:36.492000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "0a40ee06-8c79-47d0-97ff-28e42951f36b", + "created_date": "2024-08-16 23:58:36.876000", + "last_modified_date": "2024-08-16 23:58:36.876000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", + "volume_id": null + }, + { + "id": "0a4f93d3-bc12-40d5-ba6e-1c31f57cad6c", + "created_date": "2024-08-16 23:58:35.663000", + "last_modified_date": "2024-08-16 23:58:35.663000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", + "volume_id": null + }, + { + "id": "0a6310a0-8fec-4582-b5d9-d0cba3d1e9df", + "created_date": "2024-08-16 23:58:36.443000", + "last_modified_date": "2024-08-16 23:58:36.443000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "19", + "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", + "volume_id": null + }, + { + "id": "0a8180d1-b208-4168-b9a3-5d1f1c97a30e", + "created_date": "2024-08-16 23:58:36.855000", + "last_modified_date": "2024-08-16 23:58:36.855000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", + "volume_id": null + }, + { + "id": "0aa0918f-bbae-4709-a370-1fac7eb33993", + "created_date": "2024-08-16 23:58:35.793000", + "last_modified_date": "2024-08-16 23:58:35.793000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "b51c738d-8ba1-4451-8f89-f49c1968ac42", + "volume_id": null + }, + { + "id": "0b699566-3222-4109-b06a-d45f8b5ca07e", + "created_date": "2024-08-16 23:58:36.959000", + "last_modified_date": "2024-08-16 23:58:36.959000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "0b9dab2f-2fed-4a7e-909f-d83c483831cf", + "created_date": "2024-08-16 23:58:35.774000", + "last_modified_date": "2024-08-16 23:58:35.774000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "25", + "comic_id": "031b7570-04bc-4f56-834e-61c5792b6e5e", + "volume_id": null + }, + { + "id": "0ba141fd-4bae-4023-bdba-f6d4cfe6d762", + "created_date": "2024-08-16 23:58:36.119000", + "last_modified_date": "2024-08-16 23:58:36.119000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "17b50be7-aca7-446e-8729-3706f636d29d", + "volume_id": null + }, + { + "id": "0c8b459d-add1-456e-aa1a-e7edb6563f2b", + "created_date": "2024-08-16 23:58:36.633000", + "last_modified_date": "2024-08-16 23:58:36.633000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "0e6688d2-f347-4800-83b1-1f094b658084", + "volume_id": null + }, + { + "id": "0cee2e8f-726a-4099-8ed5-3414b45fe895", + "created_date": "2024-08-16 23:58:36.073000", + "last_modified_date": "2024-08-16 23:58:36.073000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "12", + "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", + "volume_id": null + }, + { + "id": "0d2076f6-2516-47bd-aa74-b614c557e4ad", + "created_date": "2024-08-16 23:58:36.673000", + "last_modified_date": "2024-08-16 23:58:36.673000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", + "volume_id": null + }, + { + "id": "0d2e7bc8-a6a6-4d1a-b333-85b6e8e404e4", + "created_date": "2024-08-16 23:58:36.569000", + "last_modified_date": "2024-08-16 23:58:36.569000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "45bd0c8a-845f-4ab3-9281-c31e5a3d4472", + "volume_id": null + }, + { + "id": "0d3c0bb1-4ac8-4e8b-b925-0c2408a6cd22", + "created_date": "2024-08-16 23:58:36.676000", + "last_modified_date": "2024-08-16 23:58:36.676000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", + "volume_id": null + }, + { + "id": "0d520d9f-c13a-40dc-ae9d-dfcd21e627b5", + "created_date": "2024-08-16 23:58:36.121000", + "last_modified_date": "2024-08-16 23:58:36.121000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "17b50be7-aca7-446e-8729-3706f636d29d", + "volume_id": null + }, + { + "id": "0dda9d31-2aac-4e9c-be85-948e3afe41ee", + "created_date": "2024-08-16 23:58:35.629000", + "last_modified_date": "2024-08-16 23:58:35.629000", + "version": 0, + "in_stock": 0, + "is_read": 1, + "issue_number": "7", + "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", + "volume_id": null + }, + { + "id": "0e0dcb02-ac79-4667-8fda-078e7a605c09", + "created_date": "2024-08-16 23:58:36.390000", + "last_modified_date": "2024-08-16 23:58:36.390000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "210", + "comic_id": "d24801e2-fbfe-4497-873f-4d8edb182ae4", + "volume_id": null + }, + { + "id": "0ed58cd0-ac3b-427d-bf6a-f06b35eeb975", + "created_date": "2024-08-16 23:58:36.409000", + "last_modified_date": "2024-08-16 23:58:36.409000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "fdcbf2b1-c3cb-44d8-888b-41260a87b0e4", + "volume_id": null + }, + { + "id": "0ed653c6-a0ca-45d2-88eb-7e1256dbb958", + "created_date": "2024-08-16 23:58:36.904000", + "last_modified_date": "2024-08-16 23:58:36.904000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "9aac6484-4c06-4286-815a-219aad25cc74", + "volume_id": null + }, + { + "id": "0ed7e9d6-ccbd-4dd1-82a0-31aee9c80404", + "created_date": "2024-08-16 23:58:35.840000", + "last_modified_date": "2024-08-16 23:58:35.840000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "0f49700a-e1c2-4ed1-9007-96778f77bbdc", + "created_date": "2024-08-16 23:58:36.840000", + "last_modified_date": "2024-08-16 23:58:36.840000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "9fcdd9a5-f1fb-4421-a352-9da8e2c12f81", + "volume_id": null + }, + { + "id": "0f6db6dd-6c53-4cd1-9a24-156928b41b13", + "created_date": "2024-08-16 23:58:36.046000", + "last_modified_date": "2024-08-16 23:58:36.046000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", + "volume_id": null + }, + { + "id": "0fbc698b-dfe5-4714-b367-6a9a47bd3ac6", + "created_date": "2024-08-16 23:58:36.534000", + "last_modified_date": "2024-08-16 23:58:36.534000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "2b2a07ba-c5a8-4cb8-b170-990cb941315a", + "volume_id": null + }, + { + "id": "10924942-003a-47b6-b801-ca99e7fdcbb2", + "created_date": "2024-08-16 23:58:37.078000", + "last_modified_date": "2024-08-16 23:58:37.078000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "231e65eb-eecb-4946-a643-6184b767e321", + "volume_id": null + }, + { + "id": "1135968b-ed0f-4510-9b73-df35fbaae6d8", + "created_date": "2024-08-16 23:58:35.968000", + "last_modified_date": "2024-08-16 23:58:35.968000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", + "volume_id": null + }, + { + "id": "1140b9c0-44b3-46d2-b427-10a8b908d95e", + "created_date": "2024-08-16 23:58:36.126000", + "last_modified_date": "2024-08-16 23:58:36.126000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "f3231681-cd2b-4ff9-bfa2-5d8f631bee4d", + "volume_id": null + }, + { + "id": "11dadbd4-22f1-465c-b1b4-5c6b3e1a988e", + "created_date": "2024-08-16 23:58:36.756000", + "last_modified_date": "2024-08-16 23:58:36.756000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", + "volume_id": null + }, + { + "id": "121e1529-443a-4518-a776-f53d02cedce9", + "created_date": "2024-08-16 23:58:36.532000", + "last_modified_date": "2024-08-16 23:58:36.532000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "2b2a07ba-c5a8-4cb8-b170-990cb941315a", + "volume_id": null + }, + { + "id": "1239cfe9-8018-4ec4-bd91-c7e71d334e9d", + "created_date": "2024-08-16 23:58:36.846000", + "last_modified_date": "2024-08-16 23:58:36.846000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", + "volume_id": null + }, + { + "id": "1239fafc-5793-40be-a775-39ef99b8e058", + "created_date": "2024-08-16 23:58:36.857000", + "last_modified_date": "2024-08-16 23:58:36.857000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "8", + "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", + "volume_id": null + }, + { + "id": "1325723f-0d13-40ab-bc4f-173ea6c7eab8", + "created_date": "2024-08-16 23:58:35.749000", + "last_modified_date": "2024-08-16 23:58:35.749000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", + "volume_id": null + }, + { + "id": "13ab0889-b2df-4b2d-b76c-99f12ed1fd87", + "created_date": "2024-08-16 23:58:36.643000", + "last_modified_date": "2024-08-16 23:58:36.643000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "104", + "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", + "volume_id": null + }, + { + "id": "13fbd46f-7dee-4ce2-af50-14528932cd17", + "created_date": "2024-08-16 23:58:36.771000", + "last_modified_date": "2024-08-16 23:58:36.771000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "9", + "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", + "volume_id": null + }, + { + "id": "14f025a9-6311-4791-a9bf-10bcb6bded2f", + "created_date": "2024-08-16 23:58:36.296000", + "last_modified_date": "2024-08-16 23:58:36.296000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "13281ef7-5945-49a9-b8f1-c5e8548c18ec", + "volume_id": null + }, + { + "id": "15735fd4-0f62-4577-937b-e6b6195588b0", + "created_date": "2024-08-16 23:58:36.925000", + "last_modified_date": "2024-08-16 23:58:36.925000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "df47cdb1-0b41-4baa-83ce-fcfbb1c2bd51", + "volume_id": null + }, + { + "id": "16a6d3f4-060f-4d49-8b93-7bb01e780d81", + "created_date": "2024-08-16 23:58:36.700000", + "last_modified_date": "2024-08-16 23:58:36.700000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "153", + "comic_id": "cc96b25e-b827-4ff0-a94a-b82d30ca883c", + "volume_id": null + }, + { + "id": "16dc5aff-f489-45f1-b971-7ca7891228bd", + "created_date": "2024-08-16 23:58:35.953000", + "last_modified_date": "2024-08-16 23:58:35.953000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "12", + "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", + "volume_id": null + }, + { + "id": "16fd76e8-4d6d-40be-98dd-87a89d05c7f2", + "created_date": "2024-08-16 23:58:36.372000", + "last_modified_date": "2024-08-16 23:58:36.372000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "b6713944-8d2d-4153-8f16-fe94cc4ee119", + "volume_id": null + }, + { + "id": "17258d6f-ba85-423c-b0d1-5449efe51ff1", + "created_date": "2024-08-16 23:58:36.547000", + "last_modified_date": "2024-08-16 23:58:36.547000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "bf59f643-455e-4b60-95b8-719d55437474", + "volume_id": null + }, + { + "id": "175ad074-40ff-4775-a532-80136e9dd31c", + "created_date": "2024-08-16 23:58:36.932000", + "last_modified_date": "2024-08-16 23:58:36.932000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "82db30ac-0622-4785-9a43-95ae37e54eaa", + "volume_id": null + }, + { + "id": "17a7750c-bbac-4447-9468-8f58e23ef78a", + "created_date": "2024-08-16 23:58:36.266000", + "last_modified_date": "2024-08-16 23:58:36.266000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "5d0fd720-7875-4f9f-86eb-07ee0723908f", + "volume_id": null + }, + { + "id": "1834defc-5ecb-46e8-b3d7-c0a574f041d3", + "created_date": "2024-08-16 23:58:35.800000", + "last_modified_date": "2024-08-16 23:58:35.800000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "b51c738d-8ba1-4451-8f89-f49c1968ac42", + "volume_id": null + }, + { + "id": "1846e8cc-e331-4c9c-8dc1-197a72ad1b42", + "created_date": "2024-08-16 23:58:36.136000", + "last_modified_date": "2024-08-16 23:58:36.136000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "c0543c9f-d712-4dce-9b59-2bf73b800b31", + "volume_id": null + }, + { + "id": "18d29aad-8c65-436b-8664-b5c0af98680c", + "created_date": "2024-08-16 23:58:36.536000", + "last_modified_date": "2024-08-16 23:58:36.536000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "2b2a07ba-c5a8-4cb8-b170-990cb941315a", + "volume_id": null + }, + { + "id": "18f01b7d-b603-477f-9eb1-dc41adf0b4c8", + "created_date": "2024-08-16 23:58:36.315000", + "last_modified_date": "2024-08-16 23:58:36.315000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "230efa0e-3aea-4b07-a05f-0f788f293d0b", + "volume_id": null + }, + { + "id": "196fe5dd-0234-4ed4-a87e-778c59d5ff19", + "created_date": "2024-08-16 23:58:36.250000", + "last_modified_date": "2024-08-16 23:58:36.250000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "639eed1d-3ccf-4bfa-a595-06b44a4e5b8f", + "volume_id": null + }, + { + "id": "19b41c73-09f7-4685-a12d-79cd23976468", + "created_date": "2024-08-16 23:58:36.861000", + "last_modified_date": "2024-08-16 23:58:36.861000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "10", + "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", + "volume_id": null + }, + { + "id": "19cdde53-4042-446b-ab07-8c0392527d62", + "created_date": "2024-08-16 23:58:36.842000", + "last_modified_date": "2024-08-16 23:58:36.842000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "9fcdd9a5-f1fb-4421-a352-9da8e2c12f81", + "volume_id": null + }, + { + "id": "19dc8eac-7678-44af-aa72-12583199de91", + "created_date": "2024-08-16 23:58:36.749000", + "last_modified_date": "2024-08-16 23:58:36.749000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "4f0e16a3-452f-43cd-9834-d0f4d2d5fca0", + "volume_id": null + }, + { + "id": "1a11f7aa-b5f1-47ac-b447-209c0b62dbe9", + "created_date": "2024-08-16 23:58:36.453000", + "last_modified_date": "2024-08-16 23:58:36.453000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "24", + "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", + "volume_id": null + }, + { + "id": "1b306d28-0d69-4a2f-8d8a-f61410d89ca4", + "created_date": "2024-08-16 23:58:36.865000", + "last_modified_date": "2024-08-16 23:58:36.865000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "12", + "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", + "volume_id": null + }, + { + "id": "1ba383b4-74f9-435e-ab3a-c76a1d7456c4", + "created_date": "2024-08-16 23:58:36.406000", + "last_modified_date": "2024-08-16 23:58:36.406000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "21", + "comic_id": "ed58b16e-0701-4373-befe-39118bc2d4cb", + "volume_id": null + }, + { + "id": "1c030174-7f55-497b-8240-26a312a1de89", + "created_date": "2024-08-16 23:58:36.737000", + "last_modified_date": "2024-08-16 23:58:36.737000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "8690631a-e99c-44be-887c-8fd2bb222ee1", + "volume_id": null + }, + { + "id": "1cea0ef0-62ae-44c0-b18e-3189a2db0e87", + "created_date": "2024-08-16 23:58:36.567000", + "last_modified_date": "2024-08-16 23:58:36.567000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "45bd0c8a-845f-4ab3-9281-c31e5a3d4472", + "volume_id": null + }, + { + "id": "1d11de5a-fe99-4364-8749-50d2ed083bf5", + "created_date": "2024-08-16 23:58:36.537000", + "last_modified_date": "2024-08-16 23:58:36.537000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "f4cb5b24-00ea-4249-9d09-45432c168b8f", + "volume_id": null + }, + { + "id": "1d23e7a0-69e2-492a-9a6e-53d022f18aa6", + "created_date": "2024-08-16 23:58:35.677000", + "last_modified_date": "2024-08-16 23:58:35.677000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "10", + "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", + "volume_id": null + }, + { + "id": "1d3ecddf-cf6e-4d5b-9b94-151e34c3a69c", + "created_date": "2024-08-16 23:58:36.599000", + "last_modified_date": "2024-08-16 23:58:36.599000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "20", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "1dba942b-90fc-417a-8b3d-d0987ef4cc4d", + "created_date": "2024-08-16 23:58:36.279000", + "last_modified_date": "2024-08-16 23:58:36.279000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "3e96be30-f58f-459d-bb97-cfa574b9487c", + "volume_id": null + }, + { + "id": "1de6b0ef-1a18-482b-a9ab-443409ec964d", + "created_date": "2024-08-16 23:58:35.845000", + "last_modified_date": "2024-08-16 23:58:35.845000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "9", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "1e4f0a5b-91eb-4fff-882c-97ecdc414a1e", + "created_date": "2024-08-16 23:58:36.744000", + "last_modified_date": "2024-08-16 23:58:36.744000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "fffaa1a6-5c3d-4deb-b759-35de74c65958", + "volume_id": null + }, + { + "id": "1e6f4148-351b-4af8-a617-ccbea38d22de", + "created_date": "2024-08-16 23:58:35.595000", + "last_modified_date": "2024-08-16 23:58:35.595000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "5dbe4d8b-331a-41ad-bcdc-01196dc1d58d", + "volume_id": null + }, + { + "id": "1ea7ddf0-29ba-4e0b-b451-211bcd54d449", + "created_date": "2024-08-16 23:58:37.079000", + "last_modified_date": "2024-08-16 23:58:37.079000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "b4c866cb-9461-4aa4-bc3b-ef3e63848775", + "volume_id": null + }, + { + "id": "1ecb7f36-f6d7-4f29-ae22-df5c3fc0e2ab", + "created_date": "2024-08-16 23:58:36.091000", + "last_modified_date": "2024-08-16 23:58:36.091000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "d99f789a-0d9c-4b12-bf1f-3b090fb0f1b8", + "volume_id": null + }, + { + "id": "1f785cdc-fc07-4e68-b2b9-aa4c757b9703", + "created_date": "2024-08-16 23:58:36.598000", + "last_modified_date": "2024-08-16 23:58:36.598000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "19", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "20042e8a-243f-43f3-99d4-3a2916116786", + "created_date": "2024-08-16 23:58:35.908000", + "last_modified_date": "2024-08-16 23:58:35.908000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "b73e5e2f-60e4-42fe-94bf-44fe3755b8b8", + "volume_id": null + }, + { + "id": "2060b03e-1ba7-4022-9a5a-5a0140b91e96", + "created_date": "2024-08-16 23:58:36.690000", + "last_modified_date": "2024-08-16 23:58:36.690000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "13", + "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", + "volume_id": null + }, + { + "id": "2122df9a-897f-41c8-b5e3-dcf3e758c007", + "created_date": "2024-08-16 23:58:36.527000", + "last_modified_date": "2024-08-16 23:58:36.527000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "26", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "213fd3d0-8017-4f78-b217-aaeda8a6519f", + "created_date": "2024-08-16 23:58:36.628000", + "last_modified_date": "2024-08-16 23:58:36.628000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "0e6688d2-f347-4800-83b1-1f094b658084", + "volume_id": null + }, + { + "id": "2192ceb5-2129-4a27-ab5e-486e5f2906e2", + "created_date": "2024-08-16 23:58:36.587000", + "last_modified_date": "2024-08-16 23:58:36.587000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "13", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "2211c609-b6c9-4435-8e93-7c509ab79b20", + "created_date": "2024-08-16 23:58:35.608000", + "last_modified_date": "2024-08-16 23:58:35.608000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "5dbe4d8b-331a-41ad-bcdc-01196dc1d58d", + "volume_id": null + }, + { + "id": "222e75f6-ebc9-4892-a6dc-f26c4adbb9a7", + "created_date": "2024-08-16 23:58:36.028000", + "last_modified_date": "2024-08-16 23:58:36.028000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "17", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", + "volume_id": null + }, + { + "id": "2234d1bb-046b-4b0d-a779-3cf549370bb7", + "created_date": "2024-08-16 23:58:36.839000", + "last_modified_date": "2024-08-16 23:58:36.839000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "9fcdd9a5-f1fb-4421-a352-9da8e2c12f81", + "volume_id": null + }, + { + "id": "22d0814c-11b7-4117-a831-a706ca3ed1d4", + "created_date": "2024-08-16 23:58:36.566000", + "last_modified_date": "2024-08-16 23:58:36.566000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "45bd0c8a-845f-4ab3-9281-c31e5a3d4472", + "volume_id": null + }, + { + "id": "23a19e43-d4b9-4ac3-b9e1-17ef97be760e", + "created_date": "2024-08-16 23:58:36.612000", + "last_modified_date": "2024-08-16 23:58:36.612000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "28", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "23b6c8ff-8287-4d0c-9bc5-36bcad06fa06", + "created_date": "2024-08-16 23:58:36.370000", + "last_modified_date": "2024-08-16 23:58:36.370000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "b6713944-8d2d-4153-8f16-fe94cc4ee119", + "volume_id": null + }, + { + "id": "23be1e0b-49cb-4d24-b70a-30d612f9c79a", + "created_date": "2024-08-16 23:58:36.271000", + "last_modified_date": "2024-08-16 23:58:36.271000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "39536665-0c93-439f-97d4-be1f40da34dd", + "volume_id": null + }, + { + "id": "2419876b-d084-4d80-953e-8f4104c3ca04", + "created_date": "2024-08-16 23:58:36.722000", + "last_modified_date": "2024-08-16 23:58:36.722000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "25841b05-c246-4484-9ef2-71f8dcdb39ed", + "volume_id": null + }, + { + "id": "24246364-1514-4c54-914a-f02fdf2bb111", + "created_date": "2024-08-16 23:58:36.970000", + "last_modified_date": "2024-08-16 23:58:36.970000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "8", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "242cac83-e935-43f9-a216-2bc46c2b25b2", + "created_date": "2024-08-16 23:58:36.820000", + "last_modified_date": "2024-08-16 23:58:36.820000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "bba808d8-ede8-49fe-9ed5-3c23c7ca0f3c", + "volume_id": null + }, + { + "id": "260bd792-67ca-47b7-a930-e888956c75b9", + "created_date": "2024-08-16 23:58:36.508000", + "last_modified_date": "2024-08-16 23:58:36.508000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "17", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "260f0244-dbc9-4cc8-8fec-37664f4ebfe0", + "created_date": "2024-08-16 23:58:35.733000", + "last_modified_date": "2024-08-16 23:58:35.733000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "20", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "263a2c72-1834-4278-935a-076cd299544c", + "created_date": "2024-08-16 23:58:36.992000", + "last_modified_date": "2024-08-16 23:58:36.992000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "21", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "2645f158-2c38-40ac-9c66-0b680d1a21a3", + "created_date": "2024-08-16 23:58:36.312000", + "last_modified_date": "2024-08-16 23:58:36.312000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "60deca87-7b2a-4412-855f-01a6ccaeea56", + "volume_id": null + }, + { + "id": "2687c901-206c-4cb2-a0e9-1df1c0ed48ef", + "created_date": "2024-08-16 23:58:36.890000", + "last_modified_date": "2024-08-16 23:58:36.890000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "12", + "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", + "volume_id": null + }, + { + "id": "26e57eaa-9566-482a-842e-b00102f280b9", + "created_date": "2024-08-16 23:58:35.939000", + "last_modified_date": "2024-08-16 23:58:35.939000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "8", + "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", + "volume_id": null + }, + { + "id": "283152f1-38e9-4b2e-ac0b-f80dcdf8b8ad", + "created_date": "2024-08-16 23:58:37.042000", + "last_modified_date": "2024-08-16 23:58:37.042000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "525", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "2886cc4b-8f21-4d0a-b4ce-18dd3d4ae939", + "created_date": "2024-08-16 23:58:36.146000", + "last_modified_date": "2024-08-16 23:58:36.146000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "c0543c9f-d712-4dce-9b59-2bf73b800b31", + "volume_id": null + }, + { + "id": "28e70d59-638a-45da-b273-ec55085cd053", + "created_date": "2024-08-16 23:58:36.682000", + "last_modified_date": "2024-08-16 23:58:36.682000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "9", + "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", + "volume_id": null + }, + { + "id": "292051bb-cb8b-4989-8e00-75b6a677faaa", + "created_date": "2024-08-16 23:58:36.874000", + "last_modified_date": "2024-08-16 23:58:36.874000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", + "volume_id": null + }, + { + "id": "29526a85-abec-4fc2-91a3-bce5b8f08a6f", + "created_date": "2024-08-16 23:58:36.093000", + "last_modified_date": "2024-08-16 23:58:36.093000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "de7ef7f5-daf8-4dfd-b8de-973c902a7df0", + "volume_id": null + }, + { + "id": "299be889-336a-49e9-bba5-6c288069dee0", + "created_date": "2024-08-16 23:58:36.695000", + "last_modified_date": "2024-08-16 23:58:36.695000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "16", + "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", + "volume_id": null + }, + { + "id": "2a1bf817-518c-441d-a0ed-733a538d27b9", + "created_date": "2024-08-16 23:58:36.552000", + "last_modified_date": "2024-08-16 23:58:36.552000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "bf59f643-455e-4b60-95b8-719d55437474", + "volume_id": null + }, + { + "id": "2b54ecce-d571-44fe-a632-94c8f55b0930", + "created_date": "2024-08-16 23:58:36.678000", + "last_modified_date": "2024-08-16 23:58:36.678000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", + "volume_id": null + }, + { + "id": "2bc64196-cbd5-4be8-bd83-0a3a05240759", + "created_date": "2024-08-16 23:58:36.289000", + "last_modified_date": "2024-08-16 23:58:36.289000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "358f99c2-93f2-489d-83a4-03267fad8597", + "volume_id": null + }, + { + "id": "2bebd24c-6711-46cc-8a12-fdfa7f65b494", + "created_date": "2024-08-16 23:58:36.428000", + "last_modified_date": "2024-08-16 23:58:36.428000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "84", + "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", + "volume_id": null + }, + { + "id": "2c3f91d2-ce23-45f9-8908-d04528c16ba5", + "created_date": "2024-08-16 23:58:35.866000", + "last_modified_date": "2024-08-16 23:58:35.866000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "16", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "2c6609da-4592-4c50-94f0-a33a77dfce5c", + "created_date": "2024-08-16 23:58:36.773000", + "last_modified_date": "2024-08-16 23:58:36.773000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "11", + "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", + "volume_id": null + }, + { + "id": "2ca61b08-8840-4116-a505-517c9c5c5853", + "created_date": "2024-08-16 23:58:37.110000", + "last_modified_date": "2024-08-16 23:58:37.110000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "639f7e71-1012-49a6-bc3a-1ac7b1de3084", + "volume_id": null + }, + { + "id": "2cadaaea-4123-4d12-84e8-569ff3f16dd7", + "created_date": "2024-08-16 23:58:36.077000", + "last_modified_date": "2024-08-16 23:58:36.077000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "14", + "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", + "volume_id": null + }, + { + "id": "2cc03de7-94e8-41f4-8b0c-6d66008c445a", + "created_date": "2024-08-16 23:58:35.946000", + "last_modified_date": "2024-08-16 23:58:35.946000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "10", + "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", + "volume_id": null + }, + { + "id": "2d1f0ade-a5de-486c-bd6b-f7fa09c7340d", + "created_date": "2024-08-16 23:58:36.253000", + "last_modified_date": "2024-08-16 23:58:36.253000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "639eed1d-3ccf-4bfa-a595-06b44a4e5b8f", + "volume_id": null + }, + { + "id": "2d73deeb-f046-41e1-beaf-f7a6d7d57dc8", + "created_date": "2024-08-16 23:58:35.673000", + "last_modified_date": "2024-08-16 23:58:35.673000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "9", + "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", + "volume_id": null + }, + { + "id": "2f634ce1-2eb5-4b00-93f0-98c3551aa823", + "created_date": "2024-08-16 23:58:36.431000", + "last_modified_date": "2024-08-16 23:58:36.431000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "86", + "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", + "volume_id": null + }, + { + "id": "2f724333-b949-4f5d-88a0-ff0c951ac22e", + "created_date": "2024-08-16 23:58:36.245000", + "last_modified_date": "2024-08-16 23:58:36.245000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "639eed1d-3ccf-4bfa-a595-06b44a4e5b8f", + "volume_id": null + }, + { + "id": "2fc5ec29-4f5e-44a3-aacd-e6b553cd302d", + "created_date": "2024-08-16 23:58:35.810000", + "last_modified_date": "2024-08-16 23:58:35.810000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "89c5ea13-997a-4831-87cc-ee76ea05c71e", + "volume_id": null + }, + { + "id": "3012df9b-2cf2-4b7c-84c1-91c130e7ffee", + "created_date": "2024-08-16 23:58:36.934000", + "last_modified_date": "2024-08-16 23:58:36.934000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "20", + "comic_id": "ab909424-4ab4-4084-a47d-08ab865047e7", + "volume_id": null + }, + { + "id": "3140e945-0bc2-4c70-836e-32bcf1ac4040", + "created_date": "2024-08-16 23:58:36.911000", + "last_modified_date": "2024-08-16 23:58:36.911000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "a08aef89-634c-494c-9def-73ff7e416464", + "volume_id": null + }, + { + "id": "3209c451-135f-44e8-bcce-823729c8c624", + "created_date": "2024-08-16 23:58:36.648000", + "last_modified_date": "2024-08-16 23:58:36.648000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "107", + "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", + "volume_id": null + }, + { + "id": "322fff97-5ca7-40c0-a70c-72c2c996afb4", + "created_date": "2024-08-16 23:58:36.609000", + "last_modified_date": "2024-08-16 23:58:36.609000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "26", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "32938ea3-4f1c-4e09-a0d7-cbfa39bf1326", + "created_date": "2024-08-16 23:58:36.242000", + "last_modified_date": "2024-08-16 23:58:36.242000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "ff81bcf6-b368-4264-872c-544e85ec80e8", + "volume_id": null + }, + { + "id": "32b89dc7-9944-4beb-acbe-15893519e9f3", + "created_date": "2024-08-16 23:58:36.081000", + "last_modified_date": "2024-08-16 23:58:36.081000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "11bc20d5-bfeb-4825-9e0f-3d6954020b07", + "volume_id": null + }, + { + "id": "32bbe897-1484-4cb4-b5b3-7f2025cf0271", + "created_date": "2024-08-16 23:58:36.881000", + "last_modified_date": "2024-08-16 23:58:36.881000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", + "volume_id": null + }, + { + "id": "32c7f396-1ec0-4bf7-a970-f50e9d4733a6", + "created_date": "2024-08-16 23:58:36.657000", + "last_modified_date": "2024-08-16 23:58:36.657000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "111", + "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", + "volume_id": null + }, + { + "id": "32f52206-19ac-416c-9f37-68ca28f7ce6f", + "created_date": "2024-08-16 23:58:36.995000", + "last_modified_date": "2024-08-16 23:58:36.995000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "23", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "33ada9ac-794c-4af6-8d50-2126546daf68", + "created_date": "2024-08-16 23:58:36.277000", + "last_modified_date": "2024-08-16 23:58:36.277000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "3e96be30-f58f-459d-bb97-cfa574b9487c", + "volume_id": null + }, + { + "id": "33c30faa-9391-4025-8791-06526a527923", + "created_date": "2024-08-16 23:58:36.380000", + "last_modified_date": "2024-08-16 23:58:36.380000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "af866a6a-2b51-499a-aa2d-b46743aabafd", + "volume_id": null + }, + { + "id": "350c0c70-302c-4b2e-8eba-843931ebe8ec", + "created_date": "2024-08-16 23:58:35.656000", + "last_modified_date": "2024-08-16 23:58:35.656000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", + "volume_id": null + }, + { + "id": "35123684-12a0-4bea-be34-ab4c7cf3508b", + "created_date": "2024-08-16 23:58:36.500000", + "last_modified_date": "2024-08-16 23:58:36.500000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "12", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "3577b0bc-e00f-43d5-bccc-d5c543d4cd1d", + "created_date": "2024-08-16 23:58:36.038000", + "last_modified_date": "2024-08-16 23:58:36.038000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "bf23d317-f5b6-4cf8-8a05-4397888a82c9", + "volume_id": null + }, + { + "id": "357d559d-e57b-4d22-9a51-7391105e38f1", + "created_date": "2024-08-16 23:58:36.849000", + "last_modified_date": "2024-08-16 23:58:36.849000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", + "volume_id": null + }, + { + "id": "35c77126-e600-4973-9f02-8c136ef90f72", + "created_date": "2024-08-16 23:58:36.448000", + "last_modified_date": "2024-08-16 23:58:36.448000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "22", + "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", + "volume_id": null + }, + { + "id": "36d5081d-53d7-4eb8-836b-bf3cce2bf59b", + "created_date": "2024-08-16 23:58:36.355000", + "last_modified_date": "2024-08-16 23:58:36.355000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "c91ec109-0b4d-4fd6-995f-1a828958493f", + "volume_id": null + }, + { + "id": "370a5e12-83d8-4905-9151-89e04c393749", + "created_date": "2024-08-16 23:58:35.885000", + "last_modified_date": "2024-08-16 23:58:35.885000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "22", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "371d04f6-46fb-4233-a21a-7be740920d8a", + "created_date": "2024-08-16 23:58:36.317000", + "last_modified_date": "2024-08-16 23:58:36.317000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "28adbced-8575-4858-8150-796857bb4129", + "volume_id": null + }, + { + "id": "37c016f1-aa6a-4ba9-bb78-e1b7f6714052", + "created_date": "2024-08-16 23:58:36.183000", + "last_modified_date": "2024-08-16 23:58:36.183000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "8", + "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", + "volume_id": null + }, + { + "id": "37c234d6-6500-4f86-9be8-2ffbbd6a9ee4", + "created_date": "2024-08-16 23:58:36.906000", + "last_modified_date": "2024-08-16 23:58:36.906000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "9aac6484-4c06-4286-815a-219aad25cc74", + "volume_id": null + }, + { + "id": "37d0d861-f4e7-440f-81c9-9910fc9ef978", + "created_date": "2024-08-16 23:58:36.438000", + "last_modified_date": "2024-08-16 23:58:36.438000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "90", + "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", + "volume_id": null + }, + { + "id": "385c5087-bcb8-4cd8-bed5-ffbb9d5e2051", + "created_date": "2024-08-16 23:58:36.859000", + "last_modified_date": "2024-08-16 23:58:36.859000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "9", + "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", + "volume_id": null + }, + { + "id": "38e78ff4-f667-4a0e-9fbf-b3aa0d952670", + "created_date": "2024-08-16 23:58:36.591000", + "last_modified_date": "2024-08-16 23:58:36.591000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "16", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "3900e943-fa26-4ec0-a224-6aa2e42b86f6", + "created_date": "2024-08-16 23:58:36.524000", + "last_modified_date": "2024-08-16 23:58:36.524000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "24", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "390848ae-6ef3-4834-8e1c-c461a77da058", + "created_date": "2024-08-16 23:58:35.873000", + "last_modified_date": "2024-08-16 23:58:35.873000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "18", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "39271c8d-b64c-48dc-a239-fca5a34be74f", + "created_date": "2024-08-16 23:58:36.388000", + "last_modified_date": "2024-08-16 23:58:36.388000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "209", + "comic_id": "d24801e2-fbfe-4497-873f-4d8edb182ae4", + "volume_id": null + }, + { + "id": "39483544-36d0-44ff-ba54-7e0737db9aa8", + "created_date": "2024-08-16 23:58:36.113000", + "last_modified_date": "2024-08-16 23:58:36.113000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "0f0bfd13-f6f0-436c-ad74-5e40ea6a0cf8", + "volume_id": null + }, + { + "id": "3a4f4fd1-d004-461a-a53f-75c9b193c6ae", + "created_date": "2024-08-16 23:58:36.392000", + "last_modified_date": "2024-08-16 23:58:36.392000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "211", + "comic_id": "d24801e2-fbfe-4497-873f-4d8edb182ae4", + "volume_id": null + }, + { + "id": "3a542153-69ee-4c16-86ae-0d76f8d49fc4", + "created_date": "2024-08-16 23:58:36.829000", + "last_modified_date": "2024-08-16 23:58:36.829000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "8e293af3-05c6-4dcc-9cbf-b87512ec975b", + "volume_id": null + }, + { + "id": "3a707cb5-4464-486a-a63b-a57cf0dd851d", + "created_date": "2024-08-16 23:58:37.045000", + "last_modified_date": "2024-08-16 23:58:37.045000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "a5987e5c-0245-484d-aeb8-4b5195800d66", + "volume_id": null + }, + { + "id": "3a95141f-2ff6-4a7d-9b61-891c2284212e", + "created_date": "2024-08-16 23:58:36.884000", + "last_modified_date": "2024-08-16 23:58:36.884000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "9", + "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", + "volume_id": null + }, + { + "id": "3ac8c11e-0102-4555-a06c-e7e0eed1dddf", + "created_date": "2024-08-16 23:58:36.107000", + "last_modified_date": "2024-08-16 23:58:36.107000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "9b924cdc-8959-41e0-a84d-f3e61bbeac44", + "volume_id": null + }, + { + "id": "3b125f13-636b-4393-97e8-ea9bd23ed191", + "created_date": "2024-08-16 23:58:36.304000", + "last_modified_date": "2024-08-16 23:58:36.304000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "13281ef7-5945-49a9-b8f1-c5e8548c18ec", + "volume_id": null + }, + { + "id": "3c25513b-88a2-43af-95b8-9666fd13e232", + "created_date": "2024-08-16 23:58:36.017000", + "last_modified_date": "2024-08-16 23:58:36.017000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "12", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", + "volume_id": null + }, + { + "id": "3c65afdf-0a18-4626-9bca-aceb75a9d22a", + "created_date": "2024-08-16 23:58:36.826000", + "last_modified_date": "2024-08-16 23:58:36.826000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "8e293af3-05c6-4dcc-9cbf-b87512ec975b", + "volume_id": null + }, + { + "id": "3d4ae0be-d485-4602-a00d-b27a0bee1863", + "created_date": "2024-08-16 23:58:36.851000", + "last_modified_date": "2024-08-16 23:58:36.851000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", + "volume_id": null + }, + { + "id": "3d510703-f76b-47c1-8fd5-9e5a47016bd1", + "created_date": "2024-08-16 23:58:35.709000", + "last_modified_date": "2024-08-16 23:58:35.709000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "12", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "3e32c255-ec26-424f-a72a-1e461ae14d6b", + "created_date": "2024-08-16 23:58:35.684000", + "last_modified_date": "2024-08-16 23:58:35.684000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "3e58d82c-3421-4a59-9be3-5c1a3a05e00d", + "created_date": "2024-08-16 23:58:37.097000", + "last_modified_date": "2024-08-16 23:58:37.097000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "14a359a2-c928-4e14-9fb2-249777e6a13a", + "volume_id": null + }, + { + "id": "3ec534ad-11b3-4a20-9ff0-2a40d7d6d911", + "created_date": "2024-08-16 23:58:36.977000", + "last_modified_date": "2024-08-16 23:58:36.977000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "12", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "3f4a86d0-7479-4947-bcf2-80ecb63f2b88", + "created_date": "2024-08-16 23:58:36.458000", + "last_modified_date": "2024-08-16 23:58:36.458000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "25", + "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", + "volume_id": null + }, + { + "id": "3f50c2fa-a315-4672-b0c9-9ef752fce511", + "created_date": "2024-08-16 23:58:35.895000", + "last_modified_date": "2024-08-16 23:58:35.895000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "26", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "3f7fda95-213d-41a9-bb40-7c072b15a770", + "created_date": "2024-08-16 23:58:37.005000", + "last_modified_date": "2024-08-16 23:58:37.005000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "503", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "3ffa1cf3-930e-42a5-b890-ea01db47f849", + "created_date": "2024-08-16 23:58:36.807000", + "last_modified_date": "2024-08-16 23:58:36.807000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "61a4d2fe-44b2-41bb-a19c-f7be95d5e195", + "volume_id": null + }, + { + "id": "400fd0ab-4031-439e-962d-b89c541f7e3f", + "created_date": "2024-08-16 23:58:37.035000", + "last_modified_date": "2024-08-16 23:58:37.035000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "521", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "408e7cbc-17fd-4f30-985e-f43302283ff0", + "created_date": "2024-08-16 23:58:35.975000", + "last_modified_date": "2024-08-16 23:58:35.975000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", + "volume_id": null + }, + { + "id": "40a13eb2-1854-4f64-b4bd-ef91bcad4b79", + "created_date": "2024-08-16 23:58:36.803000", + "last_modified_date": "2024-08-16 23:58:36.803000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "14", + "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", + "volume_id": null + }, + { + "id": "40a4bf3b-9f74-4d04-9b20-221ea922a462", + "created_date": "2024-08-16 23:58:37.013000", + "last_modified_date": "2024-08-16 23:58:37.013000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "508", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "40d61175-aa4e-40d0-8161-96a22b75aaa5", + "created_date": "2024-08-16 23:58:36.243000", + "last_modified_date": "2024-08-16 23:58:36.243000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "8", + "comic_id": "ff81bcf6-b368-4264-872c-544e85ec80e8", + "volume_id": null + }, + { + "id": "410e86be-34cf-47e7-a932-9141c4aea99c", + "created_date": "2024-08-16 23:58:36.502000", + "last_modified_date": "2024-08-16 23:58:36.502000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "13", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "41629dd5-eb2a-4d8f-97fd-f1f344e6a4fc", + "created_date": "2024-08-16 23:58:36.403000", + "last_modified_date": "2024-08-16 23:58:36.403000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "19", + "comic_id": "ed58b16e-0701-4373-befe-39118bc2d4cb", + "volume_id": null + }, + { + "id": "416b0d7b-44f9-4be7-be5f-8019ca7adb5d", + "created_date": "2024-08-16 23:58:36.096000", + "last_modified_date": "2024-08-16 23:58:36.096000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "12fde8f7-8ce3-4398-8095-5bb9b5d9508d", + "volume_id": null + }, + { + "id": "41cfccad-f483-473d-b30f-d61e6d1cc4ed", + "created_date": "2024-08-16 23:58:35.883000", + "last_modified_date": "2024-08-16 23:58:35.883000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "21", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "421c3f14-bc65-4dcf-bb40-e8d7172d8336", + "created_date": "2024-08-16 23:58:36.687000", + "last_modified_date": "2024-08-16 23:58:36.687000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "12", + "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", + "volume_id": null + }, + { + "id": "422a07a1-c937-4c38-9a5f-598675a9eee3", + "created_date": "2024-08-16 23:58:36.202000", + "last_modified_date": "2024-08-16 23:58:36.202000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", + "volume_id": null + }, + { + "id": "447fe352-7a1f-4333-8a56-78fb6db5f5bb", + "created_date": "2024-08-16 23:58:36.190000", + "last_modified_date": "2024-08-16 23:58:36.190000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "12", + "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", + "volume_id": null + }, + { + "id": "448e7efb-75b6-47e3-b901-2a2dc08f027b", + "created_date": "2024-08-16 23:58:36.563000", + "last_modified_date": "2024-08-16 23:58:36.563000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "100b82bf-c134-40d9-bcc2-a8b345030b8d", + "volume_id": null + }, + { + "id": "44e71984-dac7-4f7b-b268-f096d72a9d1b", + "created_date": "2024-08-16 23:58:36.434000", + "last_modified_date": "2024-08-16 23:58:36.434000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "87", + "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", + "volume_id": null + }, + { + "id": "4540db93-a3e1-4166-baa6-ef3be86d4b99", + "created_date": "2024-08-16 23:58:37.064000", + "last_modified_date": "2024-08-16 23:58:37.064000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "20", + "comic_id": "6fd2c6fd-f8f1-4f13-892d-887b315fa4a3", + "volume_id": null + }, + { + "id": "4561aab5-ab4d-40c7-8c6f-1c92802274cb", + "created_date": "2024-08-16 23:58:36.766000", + "last_modified_date": "2024-08-16 23:58:36.766000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", + "volume_id": null + }, + { + "id": "459b1bd9-abf1-4bb5-9da7-5fe5519c48dd", + "created_date": "2024-08-16 23:58:36.685000", + "last_modified_date": "2024-08-16 23:58:36.685000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "11", + "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", + "volume_id": null + }, + { + "id": "45bccdd5-dd09-45d7-8f21-60687e8a0d14", + "created_date": "2024-08-16 23:58:35.892000", + "last_modified_date": "2024-08-16 23:58:35.892000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "25", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "46374dfc-9e16-43f4-94dd-66d082e56ada", + "created_date": "2024-08-16 23:58:36.291000", + "last_modified_date": "2024-08-16 23:58:36.291000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "358f99c2-93f2-489d-83a4-03267fad8597", + "volume_id": null + }, + { + "id": "46a1c4dd-5a08-42f2-9c9b-3141882375a4", + "created_date": "2024-08-16 23:58:35.692000", + "last_modified_date": "2024-08-16 23:58:35.692000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "46d42039-52d4-472a-86ca-fb586695b060", + "created_date": "2024-08-16 23:58:36.818000", + "last_modified_date": "2024-08-16 23:58:36.818000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "12802b17-452a-4533-a7ab-fd94f19be123", + "volume_id": null + }, + { + "id": "479b37c2-6768-4fdb-a8ec-c0778000b9bc", + "created_date": "2024-08-16 23:58:36.948000", + "last_modified_date": "2024-08-16 23:58:36.948000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "35894d94-1fb5-49de-ba77-cf2626cda939", + "volume_id": null + }, + { + "id": "479eed54-b689-46d5-ae55-dfa38c11e849", + "created_date": "2024-08-16 23:58:35.869000", + "last_modified_date": "2024-08-16 23:58:35.869000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "17", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "47b608bc-31eb-452e-84cc-657a3a8dccb7", + "created_date": "2024-08-16 23:58:35.707000", + "last_modified_date": "2024-08-16 23:58:35.707000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "11", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "4829d6ac-6182-4fc1-8019-a820ccbf50a3", + "created_date": "2024-08-16 23:58:37.028000", + "last_modified_date": "2024-08-16 23:58:37.028000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "517", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "483dc56e-2018-4770-ae8a-0b10aa0d6dc8", + "created_date": "2024-08-16 23:58:36.589000", + "last_modified_date": "2024-08-16 23:58:36.589000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "15", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "4844f763-362d-4be8-afdf-0622175f509c", + "created_date": "2024-08-16 23:58:36.417000", + "last_modified_date": "2024-08-16 23:58:36.417000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "6fff100e-050b-4da7-bdfa-10675dbab84e", + "volume_id": null + }, + { + "id": "48496963-fa12-426f-a79a-bb181cc2f665", + "created_date": "2024-08-16 23:58:36.022000", + "last_modified_date": "2024-08-16 23:58:36.022000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "14", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", + "volume_id": null + }, + { + "id": "487c1c5f-73b4-49e5-bd9c-6e5b687587e1", + "created_date": "2024-08-16 23:58:36.495000", + "last_modified_date": "2024-08-16 23:58:36.495000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "9", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "487f6363-b673-4fc1-9b83-c8a8d44f491a", + "created_date": "2024-08-16 23:58:36.987000", + "last_modified_date": "2024-08-16 23:58:36.987000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "18", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "48d66201-cf4b-411a-be42-ade3e74add24", + "created_date": "2024-08-16 23:58:36.060000", + "last_modified_date": "2024-08-16 23:58:36.060000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", + "volume_id": null + }, + { + "id": "4900f694-f054-48cb-95f9-48f9d44d24b1", + "created_date": "2024-08-16 23:58:36.785000", + "last_modified_date": "2024-08-16 23:58:36.785000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", + "volume_id": null + }, + { + "id": "4929cb43-8e8c-4ab9-a9ec-fd79be4f81ee", + "created_date": "2024-08-16 23:58:36.460000", + "last_modified_date": "2024-08-16 23:58:36.460000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "26", + "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", + "volume_id": null + }, + { + "id": "4944e5e3-1720-46bf-b8f2-57b26263b0d9", + "created_date": "2024-08-16 23:58:37.082000", + "last_modified_date": "2024-08-16 23:58:37.082000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "b4c866cb-9461-4aa4-bc3b-ef3e63848775", + "volume_id": null + }, + { + "id": "4946fe63-d1e0-4d26-8cf0-586dbb64b0f7", + "created_date": "2024-08-16 23:58:36.223000", + "last_modified_date": "2024-08-16 23:58:36.223000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "fe7fb34e-5ff2-4f6d-a649-0be19a368f46", + "volume_id": null + }, + { + "id": "4974def3-3b02-4b56-9e1c-e61c490fb63c", + "created_date": "2024-08-16 23:58:37.062000", + "last_modified_date": "2024-08-16 23:58:37.062000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "19", + "comic_id": "6fd2c6fd-f8f1-4f13-892d-887b315fa4a3", + "volume_id": null + }, + { + "id": "49e06d24-742d-47b0-856f-ee14e7bf782d", + "created_date": "2024-08-16 23:58:36.381000", + "last_modified_date": "2024-08-16 23:58:36.381000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "af866a6a-2b51-499a-aa2d-b46743aabafd", + "volume_id": null + }, + { + "id": "49f18f8a-3ec1-4c35-8d0d-7abc764d916c", + "created_date": "2024-08-16 23:58:35.911000", + "last_modified_date": "2024-08-16 23:58:35.911000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", + "volume_id": null + }, + { + "id": "4aa8b358-8fd3-45e7-8180-37c98be19fa2", + "created_date": "2024-08-16 23:58:36.497000", + "last_modified_date": "2024-08-16 23:58:36.497000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "10", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "4abcceaf-7d7e-4faa-baac-6151cc963baa", + "created_date": "2024-08-16 23:58:36.321000", + "last_modified_date": "2024-08-16 23:58:36.321000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "7c110b15-bbfc-472e-830b-b8db6ddb274e", + "volume_id": null + }, + { + "id": "4ac31ebb-0a7d-47f7-8a7f-c702207fdc02", + "created_date": "2024-08-16 23:58:36.105000", + "last_modified_date": "2024-08-16 23:58:36.105000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "9b924cdc-8959-41e0-a84d-f3e61bbeac44", + "volume_id": null + }, + { + "id": "4ac45ee8-b795-447e-9aa1-85969f4f6c07", + "created_date": "2024-08-16 23:58:36.604000", + "last_modified_date": "2024-08-16 23:58:36.604000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "23", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "4af49b06-9c1d-4e0b-b9b9-8046422bb642", + "created_date": "2024-08-16 23:58:36.482000", + "last_modified_date": "2024-08-16 23:58:36.482000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "4b1c6dda-5e3d-4199-bd66-3df62a7dc317", + "created_date": "2024-08-16 23:58:36.607000", + "last_modified_date": "2024-08-16 23:58:36.607000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "25", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "4b26c23a-8707-4f9f-a7c1-28efb1702adc", + "created_date": "2024-08-16 23:58:35.660000", + "last_modified_date": "2024-08-16 23:58:35.660000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", + "volume_id": null + }, + { + "id": "4bb55b72-636c-4666-adf6-d9b54b0cffdd", + "created_date": "2024-08-16 23:58:35.848000", + "last_modified_date": "2024-08-16 23:58:35.848000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "10", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "4bbc894d-ddce-4eca-a95f-5556a13a6ff1", + "created_date": "2024-08-16 23:58:37.116000", + "last_modified_date": "2024-08-16 23:58:37.116000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "ea903b99-4032-4b4e-add4-177e051733a8", + "volume_id": null + }, + { + "id": "4c2b1a03-5725-4c9f-b4ce-e6f7d1e2ad6e", + "created_date": "2024-08-16 23:58:36.993000", + "last_modified_date": "2024-08-16 23:58:36.993000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "22", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "4c2b775d-e3b5-4ecb-928c-b4723403dd42", + "created_date": "2024-08-16 23:58:36.742000", + "last_modified_date": "2024-08-16 23:58:36.742000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "a557dbbc-2af1-4b56-8588-8fe42cf5c454", + "volume_id": null + }, + { + "id": "4c52c4bc-ac2a-49fb-8f0b-566acf0e3168", + "created_date": "2024-08-16 23:58:35.727000", + "last_modified_date": "2024-08-16 23:58:35.727000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "18", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "4d179006-ab1e-41cf-b366-7a658f1357d1", + "created_date": "2024-08-16 23:58:36.728000", + "last_modified_date": "2024-08-16 23:58:36.728000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "fe69cf80-946a-45d6-9fba-55ebfc0f038c", + "volume_id": null + }, + { + "id": "4d797135-f34a-4d10-b1f6-2a3c955bb420", + "created_date": "2024-08-16 23:58:36.674000", + "last_modified_date": "2024-08-16 23:58:36.674000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", + "volume_id": null + }, + { + "id": "4e2f1c24-3219-48f9-a7bb-b7860375d8e1", + "created_date": "2024-08-16 23:58:36.668000", + "last_modified_date": "2024-08-16 23:58:36.668000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "c9d4b26a-8431-4f68-8e7a-b61f2cf24176", + "volume_id": null + }, + { + "id": "4e3cbf54-4374-4cf7-8247-a6457475afb2", + "created_date": "2024-08-16 23:58:36.020000", + "last_modified_date": "2024-08-16 23:58:36.020000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "13", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", + "volume_id": null + }, + { + "id": "4e3fd0ae-4c8f-4a9a-8358-07a993f9938a", + "created_date": "2024-08-16 23:58:36.901000", + "last_modified_date": "2024-08-16 23:58:36.901000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "799309bc-d8d9-4d44-9457-bae7f1e7fcbf", + "volume_id": null + }, + { + "id": "4ed9a189-d00a-465d-a75b-01df03b6d4f1", + "created_date": "2024-08-16 23:58:35.603000", + "last_modified_date": "2024-08-16 23:58:35.603000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "5dbe4d8b-331a-41ad-bcdc-01196dc1d58d", + "volume_id": null + }, + { + "id": "4f31d96b-fd07-4136-94f7-270f571a92d1", + "created_date": "2024-08-16 23:58:36.013000", + "last_modified_date": "2024-08-16 23:58:36.013000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "10", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", + "volume_id": null + }, + { + "id": "4f949f0d-8984-4c31-8143-21fac268f221", + "created_date": "2024-08-16 23:58:36.437000", + "last_modified_date": "2024-08-16 23:58:36.437000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "89", + "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", + "volume_id": null + }, + { + "id": "50870a01-229c-4623-a4aa-c96fe6d6b2b6", + "created_date": "2024-08-16 23:58:36.246000", + "last_modified_date": "2024-08-16 23:58:36.246000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "639eed1d-3ccf-4bfa-a595-06b44a4e5b8f", + "volume_id": null + }, + { + "id": "508b865c-ad22-4046-a6c3-85944ccb738e", + "created_date": "2024-08-16 23:58:35.791000", + "last_modified_date": "2024-08-16 23:58:35.791000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "5a72a63d-8fbd-46a8-b201-9b6e035c782a", + "volume_id": null + }, + { + "id": "50d633f3-56ab-462b-b5a5-5391a1c0a2f9", + "created_date": "2024-08-16 23:58:36.139000", + "last_modified_date": "2024-08-16 23:58:36.139000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "c0543c9f-d712-4dce-9b59-2bf73b800b31", + "volume_id": null + }, + { + "id": "50d8b5fe-46c0-4f0f-b565-1cc77c58f323", + "created_date": "2024-08-16 23:58:36.870000", + "last_modified_date": "2024-08-16 23:58:36.870000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "14", + "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", + "volume_id": null + }, + { + "id": "50e60c40-dddb-4cf8-91cc-c6456ddb1af0", + "created_date": "2024-08-16 23:58:35.640000", + "last_modified_date": "2024-08-16 23:58:35.640000", + "version": 0, + "in_stock": 0, + "is_read": 1, + "issue_number": "11", + "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", + "volume_id": null + }, + { + "id": "5106295e-2a16-4abf-bd36-de422a188c37", + "created_date": "2024-08-16 23:58:35.769000", + "last_modified_date": "2024-08-16 23:58:35.769000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "23", + "comic_id": "031b7570-04bc-4f56-834e-61c5792b6e5e", + "volume_id": null + }, + { + "id": "52f4f703-818b-4d55-accd-8e98645d2c31", + "created_date": "2024-08-16 23:58:35.876000", + "last_modified_date": "2024-08-16 23:58:35.876000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "19", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "53a63366-716b-424c-826e-5dfe80c76b93", + "created_date": "2024-08-16 23:58:36.357000", + "last_modified_date": "2024-08-16 23:58:36.357000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "c91ec109-0b4d-4fd6-995f-1a828958493f", + "volume_id": null + }, + { + "id": "53bdb4d5-ac28-40e7-a8dd-523c326857a0", + "created_date": "2024-08-16 23:58:36.940000", + "last_modified_date": "2024-08-16 23:58:36.940000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "41", + "comic_id": "2a4b287e-4b05-4016-8eb4-21fc225be24d", + "volume_id": null + }, + { + "id": "53c170d0-03aa-486e-bce5-57fa4c0800ea", + "created_date": "2024-08-16 23:58:36.276000", + "last_modified_date": "2024-08-16 23:58:36.276000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "3e96be30-f58f-459d-bb97-cfa574b9487c", + "volume_id": null + }, + { + "id": "53ed5350-0319-4681-9dae-1587f5c3b4fe", + "created_date": "2024-08-16 23:58:35.724000", + "last_modified_date": "2024-08-16 23:58:35.724000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "17", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "53ee8dc8-b313-4f09-90c9-f8522d65cbc7", + "created_date": "2024-08-16 23:58:36.661000", + "last_modified_date": "2024-08-16 23:58:36.661000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "c9d4b26a-8431-4f68-8e7a-b61f2cf24176", + "volume_id": null + }, + { + "id": "553d39ec-626d-4b86-ad60-eedd42fed269", + "created_date": "2024-08-16 23:58:36.834000", + "last_modified_date": "2024-08-16 23:58:36.834000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "877c8105-ea6c-4624-b458-60d18b608c15", + "volume_id": null + }, + { + "id": "553e9d19-2776-4abe-b567-dd7bf8b76f90", + "created_date": "2024-08-16 23:58:36.164000", + "last_modified_date": "2024-08-16 23:58:36.164000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "5fc6600f-b005-4d6b-a1af-a17cc2701d81", + "volume_id": null + }, + { + "id": "558a29b7-53c5-4be3-9bff-4189f6a2fdbd", + "created_date": "2024-08-16 23:58:36.760000", + "last_modified_date": "2024-08-16 23:58:36.760000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", + "volume_id": null + }, + { + "id": "560766b3-be4c-4e73-9c10-568f6cbc9c38", + "created_date": "2024-08-16 23:58:36.697000", + "last_modified_date": "2024-08-16 23:58:36.697000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "151", + "comic_id": "cc96b25e-b827-4ff0-a94a-b82d30ca883c", + "volume_id": null + }, + { + "id": "560b1722-b8bd-41ad-bc9b-85bbd965cd68", + "created_date": "2024-08-16 23:58:36.177000", + "last_modified_date": "2024-08-16 23:58:36.177000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", + "volume_id": null + }, + { + "id": "56a4c920-346a-42af-80e1-921cf249cbfe", + "created_date": "2024-08-16 23:58:35.650000", + "last_modified_date": "2024-08-16 23:58:35.650000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", + "volume_id": null + }, + { + "id": "570c91a5-462c-4280-a42a-f084edf1f69e", + "created_date": "2024-08-16 23:58:37.038000", + "last_modified_date": "2024-08-16 23:58:37.038000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "523", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "5788099a-8fec-4fbe-b656-17af6e96774f", + "created_date": "2024-08-16 23:58:36.679000", + "last_modified_date": "2024-08-16 23:58:36.679000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", + "volume_id": null + }, + { + "id": "57cd70ac-5ca4-4148-9f13-e239b4cf462c", + "created_date": "2024-08-16 23:58:37.030000", + "last_modified_date": "2024-08-16 23:58:37.030000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "518", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "580a1e4f-c0c5-486b-9626-f89daedc18db", + "created_date": "2024-08-16 23:58:36.425000", + "last_modified_date": "2024-08-16 23:58:36.425000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "82", + "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", + "volume_id": null + }, + { + "id": "58194686-7e3d-4089-a936-a26acc5400e5", + "created_date": "2024-08-16 23:58:37.130000", + "last_modified_date": "2024-08-16 23:58:37.130000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "9", + "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", + "volume_id": null + }, + { + "id": "582f3b41-b112-4c2f-a8ff-2704ac7b73c1", + "created_date": "2024-08-16 23:58:36.984000", + "last_modified_date": "2024-08-16 23:58:36.984000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "16", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "584fc3ae-1f83-4966-b4ca-7c9540f318a2", + "created_date": "2024-08-16 23:58:36.156000", + "last_modified_date": "2024-08-16 23:58:36.156000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "5fc6600f-b005-4d6b-a1af-a17cc2701d81", + "volume_id": null + }, + { + "id": "58c2f75e-9ecb-4d2c-8df3-ef37464de177", + "created_date": "2024-08-16 23:58:35.619000", + "last_modified_date": "2024-08-16 23:58:35.619000", + "version": 0, + "in_stock": 0, + "is_read": 1, + "issue_number": "4", + "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", + "volume_id": null + }, + { + "id": "59709e87-af75-4edb-a3eb-2a0e5a567fa6", + "created_date": "2024-08-16 23:58:35.984000", + "last_modified_date": "2024-08-16 23:58:35.984000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "11", + "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", + "volume_id": null + }, + { + "id": "5978e77b-b47e-4068-bc92-454fed22e1aa", + "created_date": "2024-08-16 23:58:36.777000", + "last_modified_date": "2024-08-16 23:58:36.777000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "13", + "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", + "volume_id": null + }, + { + "id": "59d9422f-9145-4150-9d48-fda6f1967829", + "created_date": "2024-08-16 23:58:37.026000", + "last_modified_date": "2024-08-16 23:58:37.026000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "516", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "5a44e6e6-df4b-4c5f-b3ad-bbe65392023f", + "created_date": "2024-08-16 23:58:35.762000", + "last_modified_date": "2024-08-16 23:58:35.762000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "21", + "comic_id": "031b7570-04bc-4f56-834e-61c5792b6e5e", + "volume_id": null + }, + { + "id": "5a90707f-2840-476e-8e58-b1dd3b692c4f", + "created_date": "2024-08-16 23:58:37.052000", + "last_modified_date": "2024-08-16 23:58:37.052000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "358d8ebf-8ada-4bc5-b47a-dd8a3b52b2c3", + "volume_id": null + }, + { + "id": "5b644415-ba50-44cd-abb5-0fa0d269792f", + "created_date": "2024-08-16 23:58:37.024000", + "last_modified_date": "2024-08-16 23:58:37.024000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "515", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "5be277f4-8e84-4bf5-8594-34320a6b9b9a", + "created_date": "2024-08-16 23:58:36.769000", + "last_modified_date": "2024-08-16 23:58:36.769000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "8", + "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", + "volume_id": null + }, + { + "id": "5c38f1c0-abb7-418a-a0d2-d0d89d12838c", + "created_date": "2024-08-16 23:58:36.571000", + "last_modified_date": "2024-08-16 23:58:36.571000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "45bd0c8a-845f-4ab3-9281-c31e5a3d4472", + "volume_id": null + }, + { + "id": "5c39c533-e7db-4dbe-8ad8-d0494dd31cd9", + "created_date": "2024-08-16 23:58:36.835000", + "last_modified_date": "2024-08-16 23:58:36.835000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "39f1943d-0620-4d88-8709-c0bf8f35790d", + "volume_id": null + }, + { + "id": "5c7e39f6-d6e9-427c-a2e1-f98ffe1464cd", + "created_date": "2024-08-16 23:58:36.941000", + "last_modified_date": "2024-08-16 23:58:36.941000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "42", + "comic_id": "2a4b287e-4b05-4016-8eb4-21fc225be24d", + "volume_id": null + }, + { + "id": "5ca1f977-741c-4f1d-be36-e3dfea16057d", + "created_date": "2024-08-16 23:58:37.135000", + "last_modified_date": "2024-08-16 23:58:37.135000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "12", + "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", + "volume_id": null + }, + { + "id": "5d02d869-8104-47af-a5e4-717879057769", + "created_date": "2024-08-16 23:58:36.828000", + "last_modified_date": "2024-08-16 23:58:36.828000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "8e293af3-05c6-4dcc-9cbf-b87512ec975b", + "volume_id": null + }, + { + "id": "5d298a9f-8cce-488d-ba62-eeb64359e9ff", + "created_date": "2024-08-16 23:58:36.473000", + "last_modified_date": "2024-08-16 23:58:36.473000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "32", + "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", + "volume_id": null + }, + { + "id": "5de11b92-cd63-43c4-801f-eaed70a7cb08", + "created_date": "2024-08-16 23:58:36.738000", + "last_modified_date": "2024-08-16 23:58:36.738000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "8690631a-e99c-44be-887c-8fd2bb222ee1", + "volume_id": null + }, + { + "id": "5e1fb9d1-7552-4483-9d3a-8d8b3df1ed2c", + "created_date": "2024-08-16 23:58:36.377000", + "last_modified_date": "2024-08-16 23:58:36.377000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "e048cec2-52b9-48f8-9975-cfe17ed85aae", + "volume_id": null + }, + { + "id": "5e269ee8-33cc-4887-9f08-a57071b43745", + "created_date": "2024-08-16 23:58:36.359000", + "last_modified_date": "2024-08-16 23:58:36.359000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "c91ec109-0b4d-4fd6-995f-1a828958493f", + "volume_id": null + }, + { + "id": "5e377def-5226-4648-a906-bc74e6eaad49", + "created_date": "2024-08-16 23:58:36.933000", + "last_modified_date": "2024-08-16 23:58:36.933000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "571cfe31-a696-41ca-8c28-ac68b17909ff", + "volume_id": null + }, + { + "id": "5e39c9e0-8e08-4b08-854d-3f945298c80b", + "created_date": "2024-08-16 23:58:36.947000", + "last_modified_date": "2024-08-16 23:58:36.947000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "35894d94-1fb5-49de-ba77-cf2626cda939", + "volume_id": null + }, + { + "id": "5e9a7d4b-56e1-4489-9ba5-023f83f41954", + "created_date": "2024-08-16 23:58:37.081000", + "last_modified_date": "2024-08-16 23:58:37.081000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "b4c866cb-9461-4aa4-bc3b-ef3e63848775", + "volume_id": null + }, + { + "id": "5ec5864c-f555-4507-8080-428da4d1a5fc", + "created_date": "2024-08-16 23:58:36.305000", + "last_modified_date": "2024-08-16 23:58:36.305000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "46", + "comic_id": "33b14231-7f52-4a1d-8909-cb00e6a241ac", + "volume_id": null + }, + { + "id": "5ee98778-ef89-4a13-8e25-4c1c46361176", + "created_date": "2024-08-16 23:58:37.033000", + "last_modified_date": "2024-08-16 23:58:37.033000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "520", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "5f0773e2-b042-40a1-b79c-7e2f3de5bd71", + "created_date": "2024-08-16 23:58:36.413000", + "last_modified_date": "2024-08-16 23:58:36.413000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "49", + "comic_id": "57965b27-1330-4921-8c0b-4b09ee06084f", + "volume_id": null + }, + { + "id": "5f2af7c9-e5a3-4d3b-8ec8-3f39d4aa6527", + "created_date": "2024-08-16 23:58:37.056000", + "last_modified_date": "2024-08-16 23:58:37.056000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "db80cac0-8598-4063-9a5f-cd4f9c0f457c", + "volume_id": null + }, + { + "id": "5f2c70b5-49dd-425b-a9d6-0b62fc575741", + "created_date": "2024-08-16 23:58:36.298000", + "last_modified_date": "2024-08-16 23:58:36.298000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "13281ef7-5945-49a9-b8f1-c5e8548c18ec", + "volume_id": null + }, + { + "id": "5f41919f-8b39-4947-adce-ec591232e243", + "created_date": "2024-08-16 23:58:36.873000", + "last_modified_date": "2024-08-16 23:58:36.873000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", + "volume_id": null + }, + { + "id": "5f7a9c3b-3cb5-4ba0-8ce5-7c56860dc912", + "created_date": "2024-08-16 23:58:35.822000", + "last_modified_date": "2024-08-16 23:58:35.822000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "5f8c388e-7c00-4e39-99a6-4419be6cf0ae", + "created_date": "2024-08-16 23:58:35.742000", + "last_modified_date": "2024-08-16 23:58:35.742000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "be424be0-a91c-47b4-b4a9-1e1d760a7177", + "volume_id": null + }, + { + "id": "5fe21289-a683-4264-a4fe-d5c3daecede9", + "created_date": "2024-08-16 23:58:35.827000", + "last_modified_date": "2024-08-16 23:58:35.827000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "60503cd0-1e4a-4b89-b7be-4231dcc34955", + "created_date": "2024-08-16 23:58:36.179000", + "last_modified_date": "2024-08-16 23:58:36.179000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", + "volume_id": null + }, + { + "id": "605bf803-637d-446b-b6dd-69460136361a", + "created_date": "2024-08-16 23:58:35.825000", + "last_modified_date": "2024-08-16 23:58:35.825000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "60e15e0e-fee2-4bb2-a1f5-aee20c522296", + "created_date": "2024-08-16 23:58:37.048000", + "last_modified_date": "2024-08-16 23:58:37.048000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "b9a8abf9-b259-4a6f-be2d-75f211788440", + "volume_id": null + }, + { + "id": "60fe7c05-28a2-428b-b984-87ba19adfda2", + "created_date": "2024-08-16 23:58:36.070000", + "last_modified_date": "2024-08-16 23:58:36.070000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "11", + "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", + "volume_id": null + }, + { + "id": "611618e6-b95c-40d8-8bb9-41381d394936", + "created_date": "2024-08-16 23:58:36.470000", + "last_modified_date": "2024-08-16 23:58:36.470000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "30", + "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", + "volume_id": null + }, + { + "id": "61363eca-f00f-4e5f-b01f-520871ea9cb3", + "created_date": "2024-08-16 23:58:36.099000", + "last_modified_date": "2024-08-16 23:58:36.099000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "12fde8f7-8ce3-4398-8095-5bb9b5d9508d", + "volume_id": null + }, + { + "id": "61bd2225-4a5b-43cc-a45e-7d2d1e5022ae", + "created_date": "2024-08-16 23:58:36.395000", + "last_modified_date": "2024-08-16 23:58:36.395000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "213", + "comic_id": "d24801e2-fbfe-4497-873f-4d8edb182ae4", + "volume_id": null + }, + { + "id": "61cf3739-2608-4cb3-a62c-315e5ce0a698", + "created_date": "2024-08-16 23:58:35.830000", + "last_modified_date": "2024-08-16 23:58:35.830000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "62d6b633-63d3-4ad0-ba57-f9b6e6f60c81", + "created_date": "2024-08-16 23:58:36.435000", + "last_modified_date": "2024-08-16 23:58:36.435000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "88", + "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", + "volume_id": null + }, + { + "id": "62ebb222-591c-4259-b440-2f011bccb889", + "created_date": "2024-08-16 23:58:36.914000", + "last_modified_date": "2024-08-16 23:58:36.914000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "0", + "comic_id": "df47cdb1-0b41-4baa-83ce-fcfbb1c2bd51", + "volume_id": null + }, + { + "id": "6308afaa-46b0-4441-a09b-c60ad5ffcc2f", + "created_date": "2024-08-16 23:58:35.949000", + "last_modified_date": "2024-08-16 23:58:35.949000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "11", + "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", + "volume_id": null + }, + { + "id": "636512ab-1107-4ca9-a138-5098fd8934ab", + "created_date": "2024-08-16 23:58:36.292000", + "last_modified_date": "2024-08-16 23:58:36.292000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "358f99c2-93f2-489d-83a4-03267fad8597", + "volume_id": null + }, + { + "id": "636d5e78-4916-4594-9519-c6294605f08c", + "created_date": "2024-08-16 23:58:35.776000", + "last_modified_date": "2024-08-16 23:58:35.776000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "e4002ab9-0f26-48e6-9927-63c350df8015", + "volume_id": null + }, + { + "id": "638c3f28-7006-45f4-99b0-dde81c2efdce", + "created_date": "2024-08-16 23:58:35.766000", + "last_modified_date": "2024-08-16 23:58:35.766000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "22", + "comic_id": "031b7570-04bc-4f56-834e-61c5792b6e5e", + "volume_id": null + }, + { + "id": "6431421d-4dae-49da-971e-3f6f92d837c1", + "created_date": "2024-08-16 23:58:36.601000", + "last_modified_date": "2024-08-16 23:58:36.601000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "21", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "6587fd05-ef54-4b99-bade-a9116988cda1", + "created_date": "2024-08-16 23:58:36.041000", + "last_modified_date": "2024-08-16 23:58:36.041000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "bf23d317-f5b6-4cf8-8a05-4397888a82c9", + "volume_id": null + }, + { + "id": "65dad05f-471c-4b00-bff8-833c0dd1d47e", + "created_date": "2024-08-16 23:58:36.110000", + "last_modified_date": "2024-08-16 23:58:36.110000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "0f0bfd13-f6f0-436c-ad74-5e40ea6a0cf8", + "volume_id": null + }, + { + "id": "65f0f591-904e-487b-adaa-b2cd475241ce", + "created_date": "2024-08-16 23:58:37.058000", + "last_modified_date": "2024-08-16 23:58:37.058000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "17", + "comic_id": "6fd2c6fd-f8f1-4f13-892d-887b315fa4a3", + "volume_id": null + }, + { + "id": "66297d4f-a22a-4637-bce4-b9f2f3a12921", + "created_date": "2024-08-16 23:58:35.690000", + "last_modified_date": "2024-08-16 23:58:35.690000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "665530c9-d084-43fa-8524-4c9df79d2482", + "created_date": "2024-08-16 23:58:36.982000", + "last_modified_date": "2024-08-16 23:58:36.982000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "15", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "66703810-7e60-41cf-a542-dbdc03e0cd26", + "created_date": "2024-08-16 23:58:35.879000", + "last_modified_date": "2024-08-16 23:58:35.879000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "20", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "667b8ef3-2420-45c3-9a86-cd0baf27f857", + "created_date": "2024-08-16 23:58:37.111000", + "last_modified_date": "2024-08-16 23:58:37.111000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "2ab84282-d7f5-43ef-af41-df0f756a62f7", + "volume_id": null + }, + { + "id": "6788e63c-ac05-4dfd-9291-f301b075c2d6", + "created_date": "2024-08-16 23:58:35.921000", + "last_modified_date": "2024-08-16 23:58:35.921000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", + "volume_id": null + }, + { + "id": "67ca9f0c-43bf-44be-825f-afd7752f566b", + "created_date": "2024-08-16 23:58:36.868000", + "last_modified_date": "2024-08-16 23:58:36.868000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "13", + "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", + "volume_id": null + }, + { + "id": "680cce2b-115b-4d65-8b49-1bdbd8c16db4", + "created_date": "2024-08-16 23:58:35.989000", + "last_modified_date": "2024-08-16 23:58:35.989000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", + "volume_id": null + }, + { + "id": "6838063d-f9bd-4511-9dcb-fe9afc89ec63", + "created_date": "2024-08-16 23:58:36.232000", + "last_modified_date": "2024-08-16 23:58:36.232000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "ff81bcf6-b368-4264-872c-544e85ec80e8", + "volume_id": null + }, + { + "id": "6919e636-10a4-4245-8fef-7e300a496656", + "created_date": "2024-08-16 23:58:36.398000", + "last_modified_date": "2024-08-16 23:58:36.398000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "215", + "comic_id": "d24801e2-fbfe-4497-873f-4d8edb182ae4", + "volume_id": null + }, + { + "id": "69afd474-5516-49c9-b8b3-9392f7a97bbb", + "created_date": "2024-08-16 23:58:36.795000", + "last_modified_date": "2024-08-16 23:58:36.795000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "9", + "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", + "volume_id": null + }, + { + "id": "6a22c937-eeb8-4e13-9560-b9afc0e216e3", + "created_date": "2024-08-16 23:58:36.208000", + "last_modified_date": "2024-08-16 23:58:36.208000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", + "volume_id": null + }, + { + "id": "6a8e02fc-90f0-4379-a460-6cb91b7c36b8", + "created_date": "2024-08-16 23:58:35.934000", + "last_modified_date": "2024-08-16 23:58:35.934000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", + "volume_id": null + }, + { + "id": "6b0b845b-bf0f-487e-9f60-bcc97e64038d", + "created_date": "2024-08-16 23:58:36.033000", + "last_modified_date": "2024-08-16 23:58:36.033000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "bf23d317-f5b6-4cf8-8a05-4397888a82c9", + "volume_id": null + }, + { + "id": "6b430f58-0256-4f97-8d47-b7dca655ffca", + "created_date": "2024-08-16 23:58:35.746000", + "last_modified_date": "2024-08-16 23:58:35.746000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "be424be0-a91c-47b4-b4a9-1e1d760a7177", + "volume_id": null + }, + { + "id": "6b64dfb3-d1cd-49f6-b3fe-53eb912b9978", + "created_date": "2024-08-16 23:58:35.606000", + "last_modified_date": "2024-08-16 23:58:35.606000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "5dbe4d8b-331a-41ad-bcdc-01196dc1d58d", + "volume_id": null + }, + { + "id": "6c600589-5aae-4eb8-ae34-981b35a16853", + "created_date": "2024-08-16 23:58:35.963000", + "last_modified_date": "2024-08-16 23:58:35.963000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", + "volume_id": null + }, + { + "id": "6cc634ca-8a6a-4f0f-8984-c6ef3fbd1d62", + "created_date": "2024-08-16 23:58:37.125000", + "last_modified_date": "2024-08-16 23:58:37.125000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", + "volume_id": null + }, + { + "id": "6cff1d7a-89f7-4f3f-a28d-1419557fab2c", + "created_date": "2024-08-16 23:58:36.440000", + "last_modified_date": "2024-08-16 23:58:36.440000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "91", + "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", + "volume_id": null + }, + { + "id": "6de115d6-bde2-47ef-84d2-02ee3396c594", + "created_date": "2024-08-16 23:58:36.726000", + "last_modified_date": "2024-08-16 23:58:36.726000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "25841b05-c246-4484-9ef2-71f8dcdb39ed", + "volume_id": null + }, + { + "id": "6dfaea42-c371-409b-bd54-4b866b9490c0", + "created_date": "2024-08-16 23:58:36.011000", + "last_modified_date": "2024-08-16 23:58:36.011000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "9", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", + "volume_id": null + }, + { + "id": "6e100dcb-0156-4c03-afb2-d7021bdceda0", + "created_date": "2024-08-16 23:58:35.701000", + "last_modified_date": "2024-08-16 23:58:35.701000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "9", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "6e1a2ef5-25cf-4b51-834f-a0b66861b5cb", + "created_date": "2024-08-16 23:58:37.018000", + "last_modified_date": "2024-08-16 23:58:37.018000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "511", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "6e2de560-8881-40a6-99dc-daaed77793e3", + "created_date": "2024-08-16 23:58:36.052000", + "last_modified_date": "2024-08-16 23:58:36.052000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", + "volume_id": null + }, + { + "id": "6e975aae-0d92-43c5-910e-ff450fa28490", + "created_date": "2024-08-16 23:58:35.918000", + "last_modified_date": "2024-08-16 23:58:35.918000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", + "volume_id": null + }, + { + "id": "6f0e6451-4401-466f-8fb4-41f37fd5b8df", + "created_date": "2024-08-16 23:58:35.854000", + "last_modified_date": "2024-08-16 23:58:35.854000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "12", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "6f83f43d-0064-4b84-a5a2-ef553b05ced3", + "created_date": "2024-08-16 23:58:37.065000", + "last_modified_date": "2024-08-16 23:58:37.065000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "21", + "comic_id": "6fd2c6fd-f8f1-4f13-892d-887b315fa4a3", + "volume_id": null + }, + { + "id": "6f856050-3f81-48a1-8547-1a9331f247e5", + "created_date": "2024-08-16 23:58:36.422000", + "last_modified_date": "2024-08-16 23:58:36.422000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "52f0849c-3c1c-49a0-9448-8b5b2c9b0c0c", + "volume_id": null + }, + { + "id": "6fff0fc9-6e6c-4220-8501-cba205822252", + "created_date": "2024-08-16 23:58:36.089000", + "last_modified_date": "2024-08-16 23:58:36.089000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "11bc20d5-bfeb-4825-9e0f-3d6954020b07", + "volume_id": null + }, + { + "id": "704757ab-0081-428f-8de7-d5b44fcd7142", + "created_date": "2024-08-16 23:58:36.998000", + "last_modified_date": "2024-08-16 23:58:36.998000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "851bc748-2602-4f20-826f-a59a7087d11f", + "volume_id": null + }, + { + "id": "70517bb0-e122-4875-97d1-ae1ca1c9f317", + "created_date": "2024-08-16 23:58:36.353000", + "last_modified_date": "2024-08-16 23:58:36.353000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "12", + "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", + "volume_id": null + }, + { + "id": "7082f983-0a03-475c-a94f-49b3ff7dda07", + "created_date": "2024-08-16 23:58:36.981000", + "last_modified_date": "2024-08-16 23:58:36.981000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "14", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "709f7ff9-eba5-4e9e-8d01-ea052ce94b91", + "created_date": "2024-08-16 23:58:36.274000", + "last_modified_date": "2024-08-16 23:58:36.274000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "19", + "comic_id": "0cb7d5d0-4204-41a4-8698-b8dd56f1c597", + "volume_id": null + }, + { + "id": "70e02b19-4b36-4ffe-bfba-17a36a125edf", + "created_date": "2024-08-16 23:58:36.281000", + "last_modified_date": "2024-08-16 23:58:36.281000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "3e96be30-f58f-459d-bb97-cfa574b9487c", + "volume_id": null + }, + { + "id": "7171d6d9-0799-44df-a592-6730f1790c0c", + "created_date": "2024-08-16 23:58:36.286000", + "last_modified_date": "2024-08-16 23:58:36.286000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "45f57e1d-f3aa-40f7-b89a-b02eeda10577", + "volume_id": null + }, + { + "id": "71b33e1c-8d1b-4e16-abdb-00a292c352f5", + "created_date": "2024-08-16 23:58:36.173000", + "last_modified_date": "2024-08-16 23:58:36.173000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", + "volume_id": null + }, + { + "id": "72047b9a-6eb7-4886-8133-1f68d3214d01", + "created_date": "2024-08-16 23:58:37.108000", + "last_modified_date": "2024-08-16 23:58:37.108000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "639f7e71-1012-49a6-bc3a-1ac7b1de3084", + "volume_id": null + }, + { + "id": "72480cf6-8f35-48e8-82bb-6808c589cd37", + "created_date": "2024-08-16 23:58:36.337000", + "last_modified_date": "2024-08-16 23:58:36.337000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", + "volume_id": null + }, + { + "id": "728be618-1a83-4a0e-b45c-1f60ee210047", + "created_date": "2024-08-16 23:58:37.102000", + "last_modified_date": "2024-08-16 23:58:37.102000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "07574acf-8f77-4da7-87fd-c49f40536baf", + "volume_id": null + }, + { + "id": "72ecd5b2-d918-4d3b-bfce-7526c292afa9", + "created_date": "2024-08-16 23:58:36.270000", + "last_modified_date": "2024-08-16 23:58:36.270000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "5d0fd720-7875-4f9f-86eb-07ee0723908f", + "volume_id": null + }, + { + "id": "737a78cd-9ff9-4aa9-813f-ef67fbaa00ee", + "created_date": "2024-08-16 23:58:36.659000", + "last_modified_date": "2024-08-16 23:58:36.659000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "112", + "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", + "volume_id": null + }, + { + "id": "737b85ee-60e5-4cd6-ace4-8ee1fddba69b", + "created_date": "2024-08-16 23:58:37.060000", + "last_modified_date": "2024-08-16 23:58:37.060000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "18", + "comic_id": "6fd2c6fd-f8f1-4f13-892d-887b315fa4a3", + "volume_id": null + }, + { + "id": "73b81180-54f5-4937-8d1e-5d61fe7498e8", + "created_date": "2024-08-16 23:58:36.893000", + "last_modified_date": "2024-08-16 23:58:36.893000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "0fd9969e-8e1f-4f42-94ac-fab4d2d614c2", + "volume_id": null + }, + { + "id": "73be0679-196f-4739-a54f-8a83d5e6906d", + "created_date": "2024-08-16 23:58:36.319000", + "last_modified_date": "2024-08-16 23:58:36.319000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "7c110b15-bbfc-472e-830b-b8db6ddb274e", + "volume_id": null + }, + { + "id": "73c52885-1826-4941-8144-1f3dfbafc0de", + "created_date": "2024-08-16 23:58:35.795000", + "last_modified_date": "2024-08-16 23:58:35.795000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "b51c738d-8ba1-4451-8f89-f49c1968ac42", + "volume_id": null + }, + { + "id": "74dbdadb-fa8d-4e49-bf9a-067cc81a2894", + "created_date": "2024-08-16 23:58:36.384000", + "last_modified_date": "2024-08-16 23:58:36.384000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "207", + "comic_id": "d24801e2-fbfe-4497-873f-4d8edb182ae4", + "volume_id": null + }, + { + "id": "751c3734-0a8a-4fa4-adfe-7f3729025ecb", + "created_date": "2024-08-16 23:58:35.730000", + "last_modified_date": "2024-08-16 23:58:35.730000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "19", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "75355c6b-eda6-411d-bf85-cda0a12f1a02", + "created_date": "2024-08-16 23:58:35.712000", + "last_modified_date": "2024-08-16 23:58:35.712000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "13", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "75433447-644f-4a4f-a5d7-83d123b4daa1", + "created_date": "2024-08-16 23:58:36.670000", + "last_modified_date": "2024-08-16 23:58:36.670000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", + "volume_id": null + }, + { + "id": "76ee9751-50ba-4600-8450-2ef9d537b882", + "created_date": "2024-08-16 23:58:36.004000", + "last_modified_date": "2024-08-16 23:58:36.004000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", + "volume_id": null + }, + { + "id": "77f5cdc7-0db2-4aa6-829b-9f6606408112", + "created_date": "2024-08-16 23:58:36.788000", + "last_modified_date": "2024-08-16 23:58:36.788000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", + "volume_id": null + }, + { + "id": "7884bbf8-53c1-4ac2-8801-df5ef9b29bc2", + "created_date": "2024-08-16 23:58:37.075000", + "last_modified_date": "2024-08-16 23:58:37.075000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "231e65eb-eecb-4946-a643-6184b767e321", + "volume_id": null + }, + { + "id": "79468348-8896-4ff2-b36f-da594eaa78e4", + "created_date": "2024-08-16 23:58:36.671000", + "last_modified_date": "2024-08-16 23:58:36.671000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", + "volume_id": null + }, + { + "id": "79704f8d-ca6a-4eee-acb4-bebbd2695f19", + "created_date": "2024-08-16 23:58:36.809000", + "last_modified_date": "2024-08-16 23:58:36.809000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "f4f227f0-a6d1-49fd-baea-6791245a89e7", + "volume_id": null + }, + { + "id": "79928b2c-4d50-49af-b8a8-6434359119af", + "created_date": "2024-08-16 23:58:36.541000", + "last_modified_date": "2024-08-16 23:58:36.541000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "f4cb5b24-00ea-4249-9d09-45432c168b8f", + "volume_id": null + }, + { + "id": "79ca675a-0a4f-4109-a520-65a5b5fbd0ca", + "created_date": "2024-08-16 23:58:36.468000", + "last_modified_date": "2024-08-16 23:58:36.468000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "29", + "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", + "volume_id": null + }, + { + "id": "7a628ecb-48f0-4627-ba08-f5342160b4ce", + "created_date": "2024-08-16 23:58:36.159000", + "last_modified_date": "2024-08-16 23:58:36.159000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "5fc6600f-b005-4d6b-a1af-a17cc2701d81", + "volume_id": null + }, + { + "id": "7abdddc6-f990-4eb1-8e2c-5ef97972f451", + "created_date": "2024-08-16 23:58:36.204000", + "last_modified_date": "2024-08-16 23:58:36.204000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", + "volume_id": null + }, + { + "id": "7ac72c94-7517-401b-84f6-a79829e69ed2", + "created_date": "2024-08-16 23:58:36.975000", + "last_modified_date": "2024-08-16 23:58:36.975000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "11", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "7ae8e72f-fb2a-4ef7-84e5-a1e6dd99d9d9", + "created_date": "2024-08-16 23:58:35.670000", + "last_modified_date": "2024-08-16 23:58:35.670000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "8", + "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", + "volume_id": null + }, + { + "id": "7b7a972c-fbb7-4559-a2df-ccdd899840cc", + "created_date": "2024-08-16 23:58:36.386000", + "last_modified_date": "2024-08-16 23:58:36.386000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "208", + "comic_id": "d24801e2-fbfe-4497-873f-4d8edb182ae4", + "volume_id": null + }, + { + "id": "7c195d1b-e0bd-4a5c-a0b9-b3b3c8892e8d", + "created_date": "2024-08-16 23:58:37.036000", + "last_modified_date": "2024-08-16 23:58:37.036000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "522", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "7c38db11-e097-48cc-85f4-fd36e25a2712", + "created_date": "2024-08-16 23:58:36.810000", + "last_modified_date": "2024-08-16 23:58:36.810000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "f4f227f0-a6d1-49fd-baea-6791245a89e7", + "volume_id": null + }, + { + "id": "7c9626a7-3508-4ab1-a225-8451db16502e", + "created_date": "2024-08-16 23:58:36.048000", + "last_modified_date": "2024-08-16 23:58:36.048000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", + "volume_id": null + }, + { + "id": "7cbf9370-bf97-44e2-b711-fdd785869a9b", + "created_date": "2024-08-16 23:58:35.842000", + "last_modified_date": "2024-08-16 23:58:35.842000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "8", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "7e2de3c0-3003-4b56-8ac9-6800ef97d396", + "created_date": "2024-08-16 23:58:36.908000", + "last_modified_date": "2024-08-16 23:58:36.908000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "9aac6484-4c06-4286-815a-219aad25cc74", + "volume_id": null + }, + { + "id": "7f360381-97a4-4569-8fb0-b717a4f9ddeb", + "created_date": "2024-08-16 23:58:36.780000", + "last_modified_date": "2024-08-16 23:58:36.780000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1/2", + "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", + "volume_id": null + }, + { + "id": "7f548d49-fbbe-40bf-8ee1-2a30d58dd2f3", + "created_date": "2024-08-16 23:58:36.210000", + "last_modified_date": "2024-08-16 23:58:36.210000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "8", + "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", + "volume_id": null + }, + { + "id": "7f8b3fd7-db82-427d-a717-0423a2d7fe46", + "created_date": "2024-08-16 23:58:36.529000", + "last_modified_date": "2024-08-16 23:58:36.529000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "2b2a07ba-c5a8-4cb8-b170-990cb941315a", + "volume_id": null + }, + { + "id": "7f9b94ae-2631-4801-956f-53358b878634", + "created_date": "2024-08-16 23:58:36.888000", + "last_modified_date": "2024-08-16 23:58:36.888000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "11", + "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", + "volume_id": null + }, + { + "id": "7fe8b5e2-5b6f-4231-b82c-2946e8930254", + "created_date": "2024-08-16 23:58:36.043000", + "last_modified_date": "2024-08-16 23:58:36.043000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "bf23d317-f5b6-4cf8-8a05-4397888a82c9", + "volume_id": null + }, + { + "id": "800235c2-e5af-4bee-8984-582b495338fd", + "created_date": "2024-08-16 23:58:36.102000", + "last_modified_date": "2024-08-16 23:58:36.102000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "9b924cdc-8959-41e0-a84d-f3e61bbeac44", + "volume_id": null + }, + { + "id": "80934009-0562-408a-8578-cede068974f0", + "created_date": "2024-08-16 23:58:35.901000", + "last_modified_date": "2024-08-16 23:58:35.901000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "b73e5e2f-60e4-42fe-94bf-44fe3755b8b8", + "volume_id": null + }, + { + "id": "809b12a7-e2ed-429a-b14f-a8d4eef6fb13", + "created_date": "2024-08-16 23:58:36.466000", + "last_modified_date": "2024-08-16 23:58:36.466000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "28", + "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", + "volume_id": null + }, + { + "id": "815bfce1-94fb-4867-a392-282722121559", + "created_date": "2024-08-16 23:58:36.793000", + "last_modified_date": "2024-08-16 23:58:36.793000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "8", + "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", + "volume_id": null + }, + { + "id": "817e975e-29e1-4632-b1ba-23c746a91cdc", + "created_date": "2024-08-16 23:58:36.341000", + "last_modified_date": "2024-08-16 23:58:36.341000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", + "volume_id": null + }, + { + "id": "818e9b48-8bad-49f0-910f-d7eb5349f272", + "created_date": "2024-08-16 23:58:35.754000", + "last_modified_date": "2024-08-16 23:58:35.754000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", + "volume_id": null + }, + { + "id": "81fb3fa2-ed65-4122-9418-3772e8abe8a1", + "created_date": "2024-08-16 23:58:36.521000", + "last_modified_date": "2024-08-16 23:58:36.521000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "23", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "823239ed-b2d6-4d52-ad93-784c53a63494", + "created_date": "2024-08-16 23:58:36.515000", + "last_modified_date": "2024-08-16 23:58:36.515000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "21", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "826bf417-ca46-4036-af69-3758ad5c88d4", + "created_date": "2024-08-16 23:58:36.464000", + "last_modified_date": "2024-08-16 23:58:36.464000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "27", + "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", + "volume_id": null + }, + { + "id": "8282ffb7-51d3-4c18-951f-2d0062e285b1", + "created_date": "2024-08-16 23:58:36.576000", + "last_modified_date": "2024-08-16 23:58:36.576000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "0095a769-e5e2-441e-8a38-f98743ee9529", + "volume_id": null + }, + { + "id": "82898012-4e4a-49f4-b725-74928ee0b5bf", + "created_date": "2024-08-16 23:58:36.651000", + "last_modified_date": "2024-08-16 23:58:36.651000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "108", + "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", + "volume_id": null + }, + { + "id": "82ae6cab-0f04-4ea5-ab90-b90d9b24f67d", + "created_date": "2024-08-16 23:58:36.634000", + "last_modified_date": "2024-08-16 23:58:36.634000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "0e6688d2-f347-4800-83b1-1f094b658084", + "volume_id": null + }, + { + "id": "83214bfd-008c-4740-9ec0-99cb95c33ef1", + "created_date": "2024-08-16 23:58:36.339000", + "last_modified_date": "2024-08-16 23:58:36.339000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", + "volume_id": null + }, + { + "id": "837d2d57-2f7d-4754-b4dc-83111703cad5", + "created_date": "2024-08-16 23:58:36.188000", + "last_modified_date": "2024-08-16 23:58:36.188000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "11", + "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", + "volume_id": null + }, + { + "id": "83b61494-58c4-456f-913d-66c41b578ea4", + "created_date": "2024-08-16 23:58:35.680000", + "last_modified_date": "2024-08-16 23:58:35.680000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "11", + "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", + "volume_id": null + }, + { + "id": "84726c1a-2ca0-4dba-89d5-8fea8eb05a28", + "created_date": "2024-08-16 23:58:36.424000", + "last_modified_date": "2024-08-16 23:58:36.424000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "81", + "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", + "volume_id": null + }, + { + "id": "849c5632-21b4-417a-aa3c-c6c1db71fb8a", + "created_date": "2024-08-16 23:58:36.916000", + "last_modified_date": "2024-08-16 23:58:36.916000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "df47cdb1-0b41-4baa-83ce-fcfbb1c2bd51", + "volume_id": null + }, + { + "id": "849e2544-bb49-4c8a-be4d-f4718d80c824", + "created_date": "2024-08-16 23:58:36.681000", + "last_modified_date": "2024-08-16 23:58:36.681000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "8", + "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", + "volume_id": null + }, + { + "id": "84dffba2-3e9f-4ff3-a386-96f812205343", + "created_date": "2024-08-16 23:58:36.430000", + "last_modified_date": "2024-08-16 23:58:36.430000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "85", + "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", + "volume_id": null + }, + { + "id": "858f9475-aaf8-4fbc-9018-396e751acfbd", + "created_date": "2024-08-16 23:58:36.693000", + "last_modified_date": "2024-08-16 23:58:36.693000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "15", + "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", + "volume_id": null + }, + { + "id": "871de10b-67a2-4aa0-8763-16a3d50a9343", + "created_date": "2024-08-16 23:58:36.997000", + "last_modified_date": "2024-08-16 23:58:36.997000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "24", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "873557a0-c166-4d03-a009-52fa0c89b016", + "created_date": "2024-08-16 23:58:36.393000", + "last_modified_date": "2024-08-16 23:58:36.393000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "212", + "comic_id": "d24801e2-fbfe-4497-873f-4d8edb182ae4", + "volume_id": null + }, + { + "id": "8756bfb3-440f-48bd-915c-23567ecc4698", + "created_date": "2024-08-16 23:58:36.845000", + "last_modified_date": "2024-08-16 23:58:36.845000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", + "volume_id": null + }, + { + "id": "87eee47e-e934-46c2-b136-db47886d676c", + "created_date": "2024-08-16 23:58:35.857000", + "last_modified_date": "2024-08-16 23:58:35.857000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "13", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "88339a4a-0465-41e0-917a-93f2bcd5310f", + "created_date": "2024-08-16 23:58:37.032000", + "last_modified_date": "2024-08-16 23:58:37.032000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "519", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "8913b527-3126-42cc-8396-e62067b2b779", + "created_date": "2024-08-16 23:58:36.635000", + "last_modified_date": "2024-08-16 23:58:36.635000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "0e6688d2-f347-4800-83b1-1f094b658084", + "volume_id": null + }, + { + "id": "89acf4ef-090f-4d7c-b9c4-e50100660cf3", + "created_date": "2024-08-16 23:58:36.707000", + "last_modified_date": "2024-08-16 23:58:36.707000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "a2409ef1-82c3-45d1-9c65-b8806a31e525", + "volume_id": null + }, + { + "id": "8a69689b-5e04-425e-b8df-ccc556ddcf93", + "created_date": "2024-08-16 23:58:36.065000", + "last_modified_date": "2024-08-16 23:58:36.065000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "9", + "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", + "volume_id": null + }, + { + "id": "8abf912d-b4d6-46d2-9c9d-85a83f7841db", + "created_date": "2024-08-16 23:58:36.009000", + "last_modified_date": "2024-08-16 23:58:36.009000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "8", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", + "volume_id": null + }, + { + "id": "8ae81ee3-91b3-44f8-8a3a-07255d334a11", + "created_date": "2024-08-16 23:58:36.257000", + "last_modified_date": "2024-08-16 23:58:36.257000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "5d0fd720-7875-4f9f-86eb-07ee0723908f", + "volume_id": null + }, + { + "id": "8ae84cc4-989e-41ab-9280-4ad32b2715a3", + "created_date": "2024-08-16 23:58:36.167000", + "last_modified_date": "2024-08-16 23:58:36.167000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "8", + "comic_id": "5fc6600f-b005-4d6b-a1af-a17cc2701d81", + "volume_id": null + }, + { + "id": "8bbe09d6-1810-41b1-911a-16c894dde7f6", + "created_date": "2024-08-16 23:58:36.540000", + "last_modified_date": "2024-08-16 23:58:36.540000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "f4cb5b24-00ea-4249-9d09-45432c168b8f", + "volume_id": null + }, + { + "id": "8be1a3b8-53d3-4a22-aa6e-71623ed1c2a8", + "created_date": "2024-08-16 23:58:36.404000", + "last_modified_date": "2024-08-16 23:58:36.404000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "20", + "comic_id": "ed58b16e-0701-4373-befe-39118bc2d4cb", + "volume_id": null + }, + { + "id": "8c10dd1b-5020-4210-a64d-44f259a416a3", + "created_date": "2024-08-16 23:58:37.067000", + "last_modified_date": "2024-08-16 23:58:37.067000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "22", + "comic_id": "6fd2c6fd-f8f1-4f13-892d-887b315fa4a3", + "volume_id": null + }, + { + "id": "8c7dea9b-82ae-499d-a482-dc78450db8b0", + "created_date": "2024-08-16 23:58:35.626000", + "last_modified_date": "2024-08-16 23:58:35.626000", + "version": 0, + "in_stock": 0, + "is_read": 1, + "issue_number": "6", + "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", + "volume_id": null + }, + { + "id": "8ce9347d-2b99-4603-a47e-52e0d23b88bc", + "created_date": "2024-08-16 23:58:36.283000", + "last_modified_date": "2024-08-16 23:58:36.283000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "3e96be30-f58f-459d-bb97-cfa574b9487c", + "volume_id": null + }, + { + "id": "8d5c0058-9d4d-4118-b15a-3250da1a1ecb", + "created_date": "2024-08-16 23:58:36.723000", + "last_modified_date": "2024-08-16 23:58:36.723000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "25841b05-c246-4484-9ef2-71f8dcdb39ed", + "volume_id": null + }, + { + "id": "8dcbd7e9-3f36-4953-aed9-c5fd160e49a1", + "created_date": "2024-08-16 23:58:36.162000", + "last_modified_date": "2024-08-16 23:58:36.162000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "5fc6600f-b005-4d6b-a1af-a17cc2701d81", + "volume_id": null + }, + { + "id": "8e16df26-2f24-4c4b-a4a0-1f8a7897f106", + "created_date": "2024-08-16 23:58:36.116000", + "last_modified_date": "2024-08-16 23:58:36.116000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "0f0bfd13-f6f0-436c-ad74-5e40ea6a0cf8", + "volume_id": null + }, + { + "id": "8f58929a-b9af-4833-b7fd-fdff8845a15c", + "created_date": "2024-08-16 23:58:35.644000", + "last_modified_date": "2024-08-16 23:58:35.644000", + "version": 0, + "in_stock": 0, + "is_read": 1, + "issue_number": "12", + "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", + "volume_id": null + }, + { + "id": "8f6eff88-05dc-4ff6-a270-f557b00cbb31", + "created_date": "2024-08-16 23:58:36.730000", + "last_modified_date": "2024-08-16 23:58:36.730000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "fe69cf80-946a-45d6-9fba-55ebfc0f038c", + "volume_id": null + }, + { + "id": "8fe1045a-1180-4a96-9eb8-b8ea14a62e2e", + "created_date": "2024-08-16 23:58:36.813000", + "last_modified_date": "2024-08-16 23:58:36.813000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "97685762-f1ae-4588-913f-0bdc38365360", + "volume_id": null + }, + { + "id": "9076e802-b585-476e-8939-df4706c9a7ca", + "created_date": "2024-08-16 23:58:36.768000", + "last_modified_date": "2024-08-16 23:58:36.768000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", + "volume_id": null + }, + { + "id": "9093f77d-ba1f-417e-be72-d0699e984ccb", + "created_date": "2024-08-16 23:58:36.543000", + "last_modified_date": "2024-08-16 23:58:36.543000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "bf59f643-455e-4b60-95b8-719d55437474", + "volume_id": null + }, + { + "id": "91546e89-ff1e-4caa-82be-4a995251047f", + "created_date": "2024-08-16 23:58:36.530000", + "last_modified_date": "2024-08-16 23:58:36.530000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "2b2a07ba-c5a8-4cb8-b170-990cb941315a", + "volume_id": null + }, + { + "id": "91b15408-4016-484f-bfa6-ae3109c41a3d", + "created_date": "2024-08-16 23:58:36.969000", + "last_modified_date": "2024-08-16 23:58:36.969000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "92360fa2-36ce-46cc-a1a2-528d1537ee52", + "created_date": "2024-08-16 23:58:36.217000", + "last_modified_date": "2024-08-16 23:58:36.217000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "11", + "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", + "volume_id": null + }, + { + "id": "929054d0-5dd2-42b7-8b02-583e2f8c142e", + "created_date": "2024-08-16 23:58:36.762000", + "last_modified_date": "2024-08-16 23:58:36.762000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", + "volume_id": null + }, + { + "id": "9295e4cb-61e1-4875-918e-561a4cb2bb74", + "created_date": "2024-08-16 23:58:36.617000", + "last_modified_date": "2024-08-16 23:58:36.617000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "31", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "9359a0ef-7633-46d0-a570-76adf0e0764a", + "created_date": "2024-08-16 23:58:36.812000", + "last_modified_date": "2024-08-16 23:58:36.812000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "f4f227f0-a6d1-49fd-baea-6791245a89e7", + "volume_id": null + }, + { + "id": "946ead50-ed24-4bb8-9756-fecb4c3c497f", + "created_date": "2024-08-16 23:58:36.001000", + "last_modified_date": "2024-08-16 23:58:36.001000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", + "volume_id": null + }, + { + "id": "956bdb4b-1477-4c74-8d90-77382ce6697c", + "created_date": "2024-08-16 23:58:36.184000", + "last_modified_date": "2024-08-16 23:58:36.184000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "9", + "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", + "volume_id": null + }, + { + "id": "95790d47-1e33-4be6-82bc-2fcd15d1160b", + "created_date": "2024-08-16 23:58:35.961000", + "last_modified_date": "2024-08-16 23:58:35.961000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "15", + "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", + "volume_id": null + }, + { + "id": "964b9724-cb11-4d41-a0b1-40ed37a9c4a7", + "created_date": "2024-08-16 23:58:36.109000", + "last_modified_date": "2024-08-16 23:58:36.109000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "9b924cdc-8959-41e0-a84d-f3e61bbeac44", + "volume_id": null + }, + { + "id": "966895fc-79a9-4e22-ad20-50ce89cfc438", + "created_date": "2024-08-16 23:58:37.071000", + "last_modified_date": "2024-08-16 23:58:37.071000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "23", + "comic_id": "6fd2c6fd-f8f1-4f13-892d-887b315fa4a3", + "volume_id": null + }, + { + "id": "96c09ec0-eb4d-4ee0-a8ef-924fb48173a0", + "created_date": "2024-08-16 23:58:36.144000", + "last_modified_date": "2024-08-16 23:58:36.144000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "c0543c9f-d712-4dce-9b59-2bf73b800b31", + "volume_id": null + }, + { + "id": "96eb1c84-799c-4153-b28a-df27af780653", + "created_date": "2024-08-16 23:58:36.416000", + "last_modified_date": "2024-08-16 23:58:36.416000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "ab9aa494-b791-498d-bf13-6c3c50c16667", + "volume_id": null + }, + { + "id": "97ac080a-8355-4688-9c61-306914dac18e", + "created_date": "2024-08-16 23:58:36.653000", + "last_modified_date": "2024-08-16 23:58:36.653000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "109", + "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", + "volume_id": null + }, + { + "id": "98637c7e-90a7-43ba-b95c-ace797117f60", + "created_date": "2024-08-16 23:58:36.364000", + "last_modified_date": "2024-08-16 23:58:36.364000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "c91ec109-0b4d-4fd6-995f-1a828958493f", + "volume_id": null + }, + { + "id": "9981ba1b-7a04-4b67-bc2a-44ed25fafa1a", + "created_date": "2024-08-16 23:58:36.602000", + "last_modified_date": "2024-08-16 23:58:36.602000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "22", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "99a06cad-625a-4b6e-a491-4951f3a1b929", + "created_date": "2024-08-16 23:58:36.655000", + "last_modified_date": "2024-08-16 23:58:36.655000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "110", + "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", + "volume_id": null + }, + { + "id": "99ac1698-3760-4aac-9c8a-41ae301eb963", + "created_date": "2024-08-16 23:58:37.118000", + "last_modified_date": "2024-08-16 23:58:37.118000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "6d3e9abd-1c42-4024-903f-6b139571da25", + "volume_id": null + }, + { + "id": "9b02efc5-0dcc-461b-b3e8-d48500f7a7b3", + "created_date": "2024-08-16 23:58:36.930000", + "last_modified_date": "2024-08-16 23:58:36.930000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "fa203e3e-db4a-44ed-beab-df526fec838c", + "volume_id": null + }, + { + "id": "9b237640-e735-4e3f-99cc-35f5bee0dbcd", + "created_date": "2024-08-16 23:58:35.978000", + "last_modified_date": "2024-08-16 23:58:35.978000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "8", + "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", + "volume_id": null + }, + { + "id": "9b3b14a3-8b8d-4283-b4ff-c15dcb3e237b", + "created_date": "2024-08-16 23:58:35.682000", + "last_modified_date": "2024-08-16 23:58:35.682000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "9c14793e-c4d9-43fd-a81e-f75345990b8a", + "created_date": "2024-08-16 23:58:36.255000", + "last_modified_date": "2024-08-16 23:58:36.255000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "639eed1d-3ccf-4bfa-a595-06b44a4e5b8f", + "volume_id": null + }, + { + "id": "9c34949c-821e-4f2d-b2c7-0fd8e8274cc1", + "created_date": "2024-08-16 23:58:36.213000", + "last_modified_date": "2024-08-16 23:58:36.213000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "9", + "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", + "volume_id": null + }, + { + "id": "9cb42533-c7d2-415b-9fdb-426db8f44233", + "created_date": "2024-08-16 23:58:36.596000", + "last_modified_date": "2024-08-16 23:58:36.596000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "18", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "9cdd4cce-8c13-4210-951a-c2cae94f0fb5", + "created_date": "2024-08-16 23:58:36.401000", + "last_modified_date": "2024-08-16 23:58:36.401000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "18", + "comic_id": "ed58b16e-0701-4373-befe-39118bc2d4cb", + "volume_id": null + }, + { + "id": "9d059360-cab1-48bc-b465-bb20ed0d5aa3", + "created_date": "2024-08-16 23:58:36.903000", + "last_modified_date": "2024-08-16 23:58:36.903000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "9aac6484-4c06-4286-815a-219aad25cc74", + "volume_id": null + }, + { + "id": "9d409127-97ed-4a4a-a88c-ef093b06fc79", + "created_date": "2024-08-16 23:58:36.378000", + "last_modified_date": "2024-08-16 23:58:36.378000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "af866a6a-2b51-499a-aa2d-b46743aabafd", + "volume_id": null + }, + { + "id": "9d5ef04a-4da7-49aa-a049-037e6b7430f7", + "created_date": "2024-08-16 23:58:35.958000", + "last_modified_date": "2024-08-16 23:58:35.958000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "14", + "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", + "volume_id": null + }, + { + "id": "9e620fb4-5300-42a7-b6d4-51efa0fe96c5", + "created_date": "2024-08-16 23:58:36.786000", + "last_modified_date": "2024-08-16 23:58:36.786000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", + "volume_id": null + }, + { + "id": "9edb244c-a3ea-44aa-aa66-2200ea07ef13", + "created_date": "2024-08-16 23:58:36.734000", + "last_modified_date": "2024-08-16 23:58:36.734000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "fe69cf80-946a-45d6-9fba-55ebfc0f038c", + "volume_id": null + }, + { + "id": "9f1c1f28-8d89-4efc-8004-8a8abb7af325", + "created_date": "2024-08-16 23:58:36.366000", + "last_modified_date": "2024-08-16 23:58:36.366000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "c91ec109-0b4d-4fd6-995f-1a828958493f", + "volume_id": null + }, + { + "id": "9fb6c042-10a2-4188-91ef-d83a02e8d364", + "created_date": "2024-08-16 23:58:35.973000", + "last_modified_date": "2024-08-16 23:58:35.973000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", + "volume_id": null + }, + { + "id": "a00e0a5f-aa6a-4838-8a2f-8c2101d45160", + "created_date": "2024-08-16 23:58:36.646000", + "last_modified_date": "2024-08-16 23:58:36.646000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "106", + "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", + "volume_id": null + }, + { + "id": "a12a705a-2efe-41f4-b3de-f48136cdb133", + "created_date": "2024-08-16 23:58:36.407000", + "last_modified_date": "2024-08-16 23:58:36.407000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "22", + "comic_id": "ed58b16e-0701-4373-befe-39118bc2d4cb", + "volume_id": null + }, + { + "id": "a1391e84-47b9-46c1-ac1b-5adebee90fcf", + "created_date": "2024-08-16 23:58:37.088000", + "last_modified_date": "2024-08-16 23:58:37.088000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "b4c866cb-9461-4aa4-bc3b-ef3e63848775", + "volume_id": null + }, + { + "id": "a1e17398-ca62-47d7-825c-8c7284c192ae", + "created_date": "2024-08-16 23:58:35.929000", + "last_modified_date": "2024-08-16 23:58:35.929000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", + "volume_id": null + }, + { + "id": "a24d1ab0-f050-4b77-a68e-c984146bbd2c", + "created_date": "2024-08-16 23:58:36.207000", + "last_modified_date": "2024-08-16 23:58:36.207000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", + "volume_id": null + }, + { + "id": "a274f0a4-e958-4146-bc08-a9b48e8e6b1c", + "created_date": "2024-08-16 23:58:36.141000", + "last_modified_date": "2024-08-16 23:58:36.141000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "c0543c9f-d712-4dce-9b59-2bf73b800b31", + "volume_id": null + }, + { + "id": "a277d0f5-6c96-4c05-96ec-c3d807a5ded0", + "created_date": "2024-08-16 23:58:35.861000", + "last_modified_date": "2024-08-16 23:58:35.861000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "14", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "a301150f-bf42-43f2-97b1-0142d16ad072", + "created_date": "2024-08-16 23:58:36.300000", + "last_modified_date": "2024-08-16 23:58:36.300000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "13281ef7-5945-49a9-b8f1-c5e8548c18ec", + "volume_id": null + }, + { + "id": "a31d9488-e89a-46bd-b5dd-37c90f38b416", + "created_date": "2024-08-16 23:58:36.631000", + "last_modified_date": "2024-08-16 23:58:36.631000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "0e6688d2-f347-4800-83b1-1f094b658084", + "volume_id": null + }, + { + "id": "a345f5e8-e740-4da3-b371-bd35cb4bc16e", + "created_date": "2024-08-16 23:58:36.792000", + "last_modified_date": "2024-08-16 23:58:36.792000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", + "volume_id": null + }, + { + "id": "a3b6f49c-081a-46c0-8a3d-cea66bb216b9", + "created_date": "2024-08-16 23:58:36.487000", + "last_modified_date": "2024-08-16 23:58:36.487000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "a499d5fc-543e-49e5-89f9-b39f1f170a5d", + "created_date": "2024-08-16 23:58:35.771000", + "last_modified_date": "2024-08-16 23:58:35.771000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "24", + "comic_id": "031b7570-04bc-4f56-834e-61c5792b6e5e", + "volume_id": null + }, + { + "id": "a49edcea-c031-452b-8877-283b85d8f95d", + "created_date": "2024-08-16 23:58:36.621000", + "last_modified_date": "2024-08-16 23:58:36.621000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "34", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "a5379fc8-0a35-4caa-978a-7d36b79cc688", + "created_date": "2024-08-16 23:58:35.686000", + "last_modified_date": "2024-08-16 23:58:35.686000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "a5e080b6-cd83-42b2-aef6-502d8d83dc9e", + "created_date": "2024-08-16 23:58:35.720000", + "last_modified_date": "2024-08-16 23:58:35.720000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "16", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "a6445817-0a9d-4696-8101-f37403e22d29", + "created_date": "2024-08-16 23:58:36.104000", + "last_modified_date": "2024-08-16 23:58:36.104000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "9b924cdc-8959-41e0-a84d-f3e61bbeac44", + "volume_id": null + }, + { + "id": "a67b1f04-0cf0-41d8-8b17-5a349d808447", + "created_date": "2024-08-16 23:58:36.790000", + "last_modified_date": "2024-08-16 23:58:36.790000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", + "volume_id": null + }, + { + "id": "a68c6312-c1e9-484f-a5ba-6733311f06af", + "created_date": "2024-08-16 23:58:35.813000", + "last_modified_date": "2024-08-16 23:58:35.813000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "89c5ea13-997a-4831-87cc-ee76ea05c71e", + "volume_id": null + }, + { + "id": "a72de1b3-f5e1-4ef8-ab2c-f07cfe80c208", + "created_date": "2024-08-16 23:58:36.782000", + "last_modified_date": "2024-08-16 23:58:36.782000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", + "volume_id": null + }, + { + "id": "a750bcf0-d8c2-41aa-a7c1-2db675f8b2b2", + "created_date": "2024-08-16 23:58:36.666000", + "last_modified_date": "2024-08-16 23:58:36.666000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "c9d4b26a-8431-4f68-8e7a-b61f2cf24176", + "volume_id": null + }, + { + "id": "a7a5a3cd-fe34-49f3-81ea-0c87f569afa2", + "created_date": "2024-08-16 23:58:35.999000", + "last_modified_date": "2024-08-16 23:58:35.999000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", + "volume_id": null + }, + { + "id": "a80b9018-dc6c-4e46-b696-7074ab32e6fe", + "created_date": "2024-08-16 23:58:36.626000", + "last_modified_date": "2024-08-16 23:58:36.626000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "0e6688d2-f347-4800-83b1-1f094b658084", + "volume_id": null + }, + { + "id": "a8ee84aa-6ac2-4ec2-b43a-4eaf93bf8a9f", + "created_date": "2024-08-16 23:58:36.764000", + "last_modified_date": "2024-08-16 23:58:36.764000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", + "volume_id": null + }, + { + "id": "a92c4a61-21c9-4b29-8171-25f3619d583a", + "created_date": "2024-08-16 23:58:36.615000", + "last_modified_date": "2024-08-16 23:58:36.615000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "30", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "a933279e-9c81-4f1d-8035-6fe54d593481", + "created_date": "2024-08-16 23:58:36.240000", + "last_modified_date": "2024-08-16 23:58:36.240000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "ff81bcf6-b368-4264-872c-544e85ec80e8", + "volume_id": null + }, + { + "id": "a94d8996-1af3-4820-8615-9ae5cafbace4", + "created_date": "2024-08-16 23:58:36.314000", + "last_modified_date": "2024-08-16 23:58:36.314000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "60deca87-7b2a-4412-855f-01a6ccaeea56", + "volume_id": null + }, + { + "id": "a9a4dee8-3481-4560-8122-480cd0a9048f", + "created_date": "2024-08-16 23:58:37.124000", + "last_modified_date": "2024-08-16 23:58:37.124000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", + "volume_id": null + }, + { + "id": "a9e41efc-1851-47fb-8ac8-58ae3e55ce93", + "created_date": "2024-08-16 23:58:37.122000", + "last_modified_date": "2024-08-16 23:58:37.122000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", + "volume_id": null + }, + { + "id": "a9f6688f-c7d4-4ee4-8dbd-a7d824a7a0c9", + "created_date": "2024-08-16 23:58:35.905000", + "last_modified_date": "2024-08-16 23:58:35.905000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "b73e5e2f-60e4-42fe-94bf-44fe3755b8b8", + "volume_id": null + }, + { + "id": "aa4c1fcf-d287-4fa4-ad94-b242048a82d8", + "created_date": "2024-08-16 23:58:37.132000", + "last_modified_date": "2024-08-16 23:58:37.132000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "10", + "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", + "volume_id": null + }, + { + "id": "ab08fb7b-ca64-45a2-9f06-dffff806e486", + "created_date": "2024-08-16 23:58:36.581000", + "last_modified_date": "2024-08-16 23:58:36.581000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "8955b2a3-84d3-4b55-a1f1-d45193600861", + "volume_id": null + }, + { + "id": "ab330c71-cc81-4e26-afe8-bee99e827868", + "created_date": "2024-08-16 23:58:36.080000", + "last_modified_date": "2024-08-16 23:58:36.080000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "11bc20d5-bfeb-4825-9e0f-3d6954020b07", + "volume_id": null + }, + { + "id": "ab5fdaf3-bb34-4b79-9f1c-c2d4671ffd3f", + "created_date": "2024-08-16 23:58:37.007000", + "last_modified_date": "2024-08-16 23:58:37.007000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "504", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "abbb4cd1-0bb1-4400-954e-1883f34af959", + "created_date": "2024-08-16 23:58:36.585000", + "last_modified_date": "2024-08-16 23:58:36.585000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "8955b2a3-84d3-4b55-a1f1-d45193600861", + "volume_id": null + }, + { + "id": "ac23dade-2c8b-45f0-82b7-e5bc091cd23a", + "created_date": "2024-08-16 23:58:36.784000", + "last_modified_date": "2024-08-16 23:58:36.784000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", + "volume_id": null + }, + { + "id": "ac4538d9-608c-4e34-9429-54e22fd34a2b", + "created_date": "2024-08-16 23:58:35.863000", + "last_modified_date": "2024-08-16 23:58:35.863000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "15", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "acac22ea-ceb6-4e24-a282-fa9ee6f7da34", + "created_date": "2024-08-16 23:58:36.284000", + "last_modified_date": "2024-08-16 23:58:36.284000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "3e96be30-f58f-459d-bb97-cfa574b9487c", + "volume_id": null + }, + { + "id": "ad4fece4-77bc-4881-8dbf-99111dc83ca8", + "created_date": "2024-08-16 23:58:36.745000", + "last_modified_date": "2024-08-16 23:58:36.745000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "fffaa1a6-5c3d-4deb-b759-35de74c65958", + "volume_id": null + }, + { + "id": "ad75d30b-3d98-4391-bfca-6e4cdbef806c", + "created_date": "2024-08-16 23:58:36.711000", + "last_modified_date": "2024-08-16 23:58:36.711000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "e1ff1410-ac3a-4ba5-8503-1fab529e50c0", + "volume_id": null + }, + { + "id": "ad7a91f0-696c-4e0e-bbe7-e65338ccd209", + "created_date": "2024-08-16 23:58:36.910000", + "last_modified_date": "2024-08-16 23:58:36.910000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "a08aef89-634c-494c-9def-73ff7e416464", + "volume_id": null + }, + { + "id": "ae1bc120-ffc2-4185-bf3f-80ee7aec2592", + "created_date": "2024-08-16 23:58:36.775000", + "last_modified_date": "2024-08-16 23:58:36.775000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "12", + "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", + "volume_id": null + }, + { + "id": "ae58fdba-00e6-4ec1-ba8e-e6cc52bc3f9b", + "created_date": "2024-08-16 23:58:35.623000", + "last_modified_date": "2024-08-16 23:58:35.623000", + "version": 0, + "in_stock": 0, + "is_read": 1, + "issue_number": "5", + "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", + "volume_id": null + }, + { + "id": "af196c8b-b378-4744-89cd-2ed48d09dc1c", + "created_date": "2024-08-16 23:58:37.010000", + "last_modified_date": "2024-08-16 23:58:37.010000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "506", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "af4ee406-36dc-4bfe-8012-58347773089e", + "created_date": "2024-08-16 23:58:36.007000", + "last_modified_date": "2024-08-16 23:58:36.007000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", + "volume_id": null + }, + { + "id": "af50b4f3-4347-448d-afb0-e3f4d933397a", + "created_date": "2024-08-16 23:58:36.191000", + "last_modified_date": "2024-08-16 23:58:36.191000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "fdccf7d0-db2c-4b01-a412-66e3ff043fe9", + "volume_id": null + }, + { + "id": "af63a085-daf4-4fdc-8e45-98b5ce65b889", + "created_date": "2024-08-16 23:58:36.592000", + "last_modified_date": "2024-08-16 23:58:36.592000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "17", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "af8ee98d-a3d1-4c68-a6c7-5ca433c5ee81", + "created_date": "2024-08-16 23:58:36.936000", + "last_modified_date": "2024-08-16 23:58:36.936000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "39", + "comic_id": "2a4b287e-4b05-4016-8eb4-21fc225be24d", + "volume_id": null + }, + { + "id": "b0a7be80-d8c1-4a3d-8cdc-c03b16def307", + "created_date": "2024-08-16 23:58:36.957000", + "last_modified_date": "2024-08-16 23:58:36.957000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "b0d12a8b-92d1-4550-bc8b-75ae6627db12", + "created_date": "2024-08-16 23:58:37.015000", + "last_modified_date": "2024-08-16 23:58:37.015000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "509", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "b23d0b43-5722-4b98-8ae8-81e09559f72f", + "created_date": "2024-08-16 23:58:36.477000", + "last_modified_date": "2024-08-16 23:58:36.477000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "33", + "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", + "volume_id": null + }, + { + "id": "b2806d77-812f-405b-a836-12cc2cb5ec9a", + "created_date": "2024-08-16 23:58:35.705000", + "last_modified_date": "2024-08-16 23:58:35.705000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "10", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "b2b3bda2-b141-46d6-b5d4-4bfa0062bd58", + "created_date": "2024-08-16 23:58:36.878000", + "last_modified_date": "2024-08-16 23:58:36.878000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", + "volume_id": null + }, + { + "id": "b390ad58-37d0-4cda-97fe-976976e937d7", + "created_date": "2024-08-16 23:58:35.805000", + "last_modified_date": "2024-08-16 23:58:35.805000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "b51c738d-8ba1-4451-8f89-f49c1968ac42", + "volume_id": null + }, + { + "id": "b3d78db2-794e-4498-8b68-3575d146736b", + "created_date": "2024-08-16 23:58:37.054000", + "last_modified_date": "2024-08-16 23:58:37.054000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "ce879e17-f391-4de0-81c5-3279cbc87fc8", + "volume_id": null + }, + { + "id": "b40e2560-309c-4184-afdf-64cfb124c57a", + "created_date": "2024-08-16 23:58:35.717000", + "last_modified_date": "2024-08-16 23:58:35.717000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "15", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "b481b432-dc5a-404c-9b7c-44fab39c349b", + "created_date": "2024-08-16 23:58:35.759000", + "last_modified_date": "2024-08-16 23:58:35.759000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "20", + "comic_id": "031b7570-04bc-4f56-834e-61c5792b6e5e", + "volume_id": null + }, + { + "id": "b4f7d577-cd9f-4be9-9383-452e1e832d5d", + "created_date": "2024-08-16 23:58:37.040000", + "last_modified_date": "2024-08-16 23:58:37.040000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "524", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "b5315225-d5f2-4f16-902b-051770bff973", + "created_date": "2024-08-16 23:58:36.262000", + "last_modified_date": "2024-08-16 23:58:36.262000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "5d0fd720-7875-4f9f-86eb-07ee0723908f", + "volume_id": null + }, + { + "id": "b575864b-dd9e-4544-8822-f56c49f6c5ff", + "created_date": "2024-08-16 23:58:36.864000", + "last_modified_date": "2024-08-16 23:58:36.864000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "11", + "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", + "volume_id": null + }, + { + "id": "b58c0981-7d0f-4158-aaab-b04b25290151", + "created_date": "2024-08-16 23:58:36.031000", + "last_modified_date": "2024-08-16 23:58:36.031000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "bf23d317-f5b6-4cf8-8a05-4397888a82c9", + "volume_id": null + }, + { + "id": "b5a308f9-f82f-4b28-bbfd-c9168ac292ec", + "created_date": "2024-08-16 23:58:36.055000", + "last_modified_date": "2024-08-16 23:58:36.055000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", + "volume_id": null + }, + { + "id": "b5c0baff-80a1-43e5-bd16-cc1bd1145171", + "created_date": "2024-08-16 23:58:36.015000", + "last_modified_date": "2024-08-16 23:58:36.015000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "11", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", + "volume_id": null + }, + { + "id": "b5e3881b-b949-4f2b-b899-a9db5d4dd48f", + "created_date": "2024-08-16 23:58:36.951000", + "last_modified_date": "2024-08-16 23:58:36.951000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "35894d94-1fb5-49de-ba77-cf2626cda939", + "volume_id": null + }, + { + "id": "b61866b2-7bf9-4b3c-bce7-7be170b23bde", + "created_date": "2024-08-16 23:58:36.150000", + "last_modified_date": "2024-08-16 23:58:36.150000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "5fc6600f-b005-4d6b-a1af-a17cc2701d81", + "volume_id": null + }, + { + "id": "b67f8cae-5eea-4bfb-a945-14cc916b4173", + "created_date": "2024-08-16 23:58:35.915000", + "last_modified_date": "2024-08-16 23:58:35.915000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", + "volume_id": null + }, + { + "id": "b6b97dc6-d72b-496a-b6fd-f2757e55bddb", + "created_date": "2024-08-16 23:58:37.105000", + "last_modified_date": "2024-08-16 23:58:37.105000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "639f7e71-1012-49a6-bc3a-1ac7b1de3084", + "volume_id": null + }, + { + "id": "b6ffc679-b36a-4f5d-9e32-2d7790a3d97a", + "created_date": "2024-08-16 23:58:36.753000", + "last_modified_date": "2024-08-16 23:58:36.753000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "4f0e16a3-452f-43cd-9834-d0f4d2d5fca0", + "volume_id": null + }, + { + "id": "b700e1ac-04f4-4523-837d-92ee60b9e76f", + "created_date": "2024-08-16 23:58:35.599000", + "last_modified_date": "2024-08-16 23:58:35.599000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "5dbe4d8b-331a-41ad-bcdc-01196dc1d58d", + "volume_id": null + }, + { + "id": "b743d1ea-8c78-42e1-bba2-099031cc4acb", + "created_date": "2024-08-16 23:58:36.447000", + "last_modified_date": "2024-08-16 23:58:36.447000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "21", + "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", + "volume_id": null + }, + { + "id": "b798b880-359e-4f49-ba1e-97c7cc89739b", + "created_date": "2024-08-16 23:58:35.739000", + "last_modified_date": "2024-08-16 23:58:35.739000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "22", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "b8746fb8-fcc5-4c79-bc34-37a5dd17ce25", + "created_date": "2024-08-16 23:58:36.525000", + "last_modified_date": "2024-08-16 23:58:36.525000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "25", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "b8a7c97a-2117-4be7-a578-094dd55d7104", + "created_date": "2024-08-16 23:58:36.573000", + "last_modified_date": "2024-08-16 23:58:36.573000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "0095a769-e5e2-441e-8a38-f98743ee9529", + "volume_id": null + }, + { + "id": "b8b60c2c-21d7-44af-953f-744061e5ca93", + "created_date": "2024-08-16 23:58:36.740000", + "last_modified_date": "2024-08-16 23:58:36.740000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "a557dbbc-2af1-4b56-8588-8fe42cf5c454", + "volume_id": null + }, + { + "id": "b8cf8ca0-3371-4afc-8b73-b64b677f521c", + "created_date": "2024-08-16 23:58:37.073000", + "last_modified_date": "2024-08-16 23:58:37.073000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "231e65eb-eecb-4946-a643-6184b767e321", + "volume_id": null + }, + { + "id": "b91a18e1-c6b7-47c0-8136-e64e48ea90e8", + "created_date": "2024-08-16 23:58:36.799000", + "last_modified_date": "2024-08-16 23:58:36.799000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "12", + "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", + "volume_id": null + }, + { + "id": "b9288042-d911-49de-aba2-623ebede9448", + "created_date": "2024-08-16 23:58:36.883000", + "last_modified_date": "2024-08-16 23:58:36.883000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "8", + "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", + "volume_id": null + }, + { + "id": "b940d779-7d53-4b14-a408-7c80ab149f53", + "created_date": "2024-08-16 23:58:36.630000", + "last_modified_date": "2024-08-16 23:58:36.630000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "0e6688d2-f347-4800-83b1-1f094b658084", + "volume_id": null + }, + { + "id": "b9bff414-cf25-4903-87aa-33dd18e6e292", + "created_date": "2024-08-16 23:58:37.084000", + "last_modified_date": "2024-08-16 23:58:37.084000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "b4c866cb-9461-4aa4-bc3b-ef3e63848775", + "volume_id": null + }, + { + "id": "b9e5781b-0db0-4802-bd2b-8e415419e281", + "created_date": "2024-08-16 23:58:36.308000", + "last_modified_date": "2024-08-16 23:58:36.308000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "60deca87-7b2a-4412-855f-01a6ccaeea56", + "volume_id": null + }, + { + "id": "ba447c99-89c1-437c-b51d-a8ae1a136786", + "created_date": "2024-08-16 23:58:36.988000", + "last_modified_date": "2024-08-16 23:58:36.988000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "19", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "ba5d88bb-5dff-4868-9c94-9d5d7f16873b", + "created_date": "2024-08-16 23:58:36.512000", + "last_modified_date": "2024-08-16 23:58:36.512000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "19", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "ba8b0cd5-ba0c-49e3-9e85-61fb968c9f4d", + "created_date": "2024-08-16 23:58:35.898000", + "last_modified_date": "2024-08-16 23:58:35.898000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "b73e5e2f-60e4-42fe-94bf-44fe3755b8b8", + "volume_id": null + }, + { + "id": "bb052694-2093-49dc-8351-151e96edd36d", + "created_date": "2024-08-16 23:58:36.489000", + "last_modified_date": "2024-08-16 23:58:36.489000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "bb0f953b-bdec-49a6-9113-a52f9d23e1bf", + "created_date": "2024-08-16 23:58:35.976000", + "last_modified_date": "2024-08-16 23:58:35.976000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", + "volume_id": null + }, + { + "id": "bb7013b3-7113-4b4f-b29f-0ab50ea0c263", + "created_date": "2024-08-16 23:58:36.399000", + "last_modified_date": "2024-08-16 23:58:36.399000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "17", + "comic_id": "ed58b16e-0701-4373-befe-39118bc2d4cb", + "volume_id": null + }, + { + "id": "bb92f351-a98a-4694-9b87-a065c4eecf5b", + "created_date": "2024-08-16 23:58:35.980000", + "last_modified_date": "2024-08-16 23:58:35.980000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "9", + "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", + "volume_id": null + }, + { + "id": "bbb9ed06-b275-49f7-8894-2227d00b312a", + "created_date": "2024-08-16 23:58:36.817000", + "last_modified_date": "2024-08-16 23:58:36.817000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "12802b17-452a-4533-a7ab-fd94f19be123", + "volume_id": null + }, + { + "id": "bc5939d5-b7a9-473a-8beb-94e149808f6a", + "created_date": "2024-08-16 23:58:36.853000", + "last_modified_date": "2024-08-16 23:58:36.853000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", + "volume_id": null + }, + { + "id": "bc8a7b49-32cb-4280-85c2-41bce35e6c20", + "created_date": "2024-08-16 23:58:36.618000", + "last_modified_date": "2024-08-16 23:58:36.618000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "32", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "bca16695-676a-4a8a-8727-b295285bb491", + "created_date": "2024-08-16 23:58:36.510000", + "last_modified_date": "2024-08-16 23:58:36.510000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "18", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "bcb95bea-43cb-4e3a-a206-5a7278b48805", + "created_date": "2024-08-16 23:58:37.050000", + "last_modified_date": "2024-08-16 23:58:37.050000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "b9a8abf9-b259-4a6f-be2d-75f211788440", + "volume_id": null + }, + { + "id": "bd50d299-efe0-43e7-98ef-d05b7cc828ad", + "created_date": "2024-08-16 23:58:36.420000", + "last_modified_date": "2024-08-16 23:58:36.420000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "3217d912-3b46-47d2-a6f8-a79c1eecfde1", + "volume_id": null + }, + { + "id": "bdd8560a-42cc-418d-a582-fabba88bb405", + "created_date": "2024-08-16 23:58:36.348000", + "last_modified_date": "2024-08-16 23:58:36.348000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "10", + "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", + "volume_id": null + }, + { + "id": "be092458-4540-4825-939d-bb45df840b06", + "created_date": "2024-08-16 23:58:36.369000", + "last_modified_date": "2024-08-16 23:58:36.369000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "b6713944-8d2d-4153-8f16-fe94cc4ee119", + "volume_id": null + }, + { + "id": "be1f11e7-3d76-4fca-98c2-4a903832c7d3", + "created_date": "2024-08-16 23:58:36.068000", + "last_modified_date": "2024-08-16 23:58:36.068000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "10", + "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", + "volume_id": null + }, + { + "id": "be21d1d9-9ee8-4c59-b245-0c5a0242d3bc", + "created_date": "2024-08-16 23:58:36.702000", + "last_modified_date": "2024-08-16 23:58:36.702000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "154", + "comic_id": "cc96b25e-b827-4ff0-a94a-b82d30ca883c", + "volume_id": null + }, + { + "id": "be368ff7-bbd0-472d-bac4-56683e9d4ae2", + "created_date": "2024-08-16 23:58:36.824000", + "last_modified_date": "2024-08-16 23:58:36.824000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "0", + "comic_id": "8e293af3-05c6-4dcc-9cbf-b87512ec975b", + "volume_id": null + }, + { + "id": "be7dc008-f661-4d60-871f-06d38cb40718", + "created_date": "2024-08-16 23:58:36.821000", + "last_modified_date": "2024-08-16 23:58:36.821000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "dc81f255-6757-4c41-8a7b-a06a503d7daa", + "volume_id": null + }, + { + "id": "bf1e55ce-7152-4cab-8f0f-3e728060d6f4", + "created_date": "2024-08-16 23:58:35.613000", + "last_modified_date": "2024-08-16 23:58:35.613000", + "version": 0, + "in_stock": 0, + "is_read": 1, + "issue_number": "2", + "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", + "volume_id": null + }, + { + "id": "bf7bad86-d121-42da-b4a1-fe139b1bf47f", + "created_date": "2024-08-16 23:58:35.816000", + "last_modified_date": "2024-08-16 23:58:35.816000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "89c5ea13-997a-4831-87cc-ee76ea05c71e", + "volume_id": null + }, + { + "id": "bf7ed47b-4c54-4b8d-8add-a85c4eee7d3a", + "created_date": "2024-08-16 23:58:35.757000", + "last_modified_date": "2024-08-16 23:58:35.757000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "11", + "comic_id": "d7dd6d02-bc9a-4fbf-a5ca-ad4728cb8109", + "volume_id": null + }, + { + "id": "c03383be-6867-4e9c-af5d-c9354e9ec268", + "created_date": "2024-08-16 23:58:36.129000", + "last_modified_date": "2024-08-16 23:58:36.129000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "f3231681-cd2b-4ff9-bfa2-5d8f631bee4d", + "volume_id": null + }, + { + "id": "c121830e-9454-42f9-a7d4-909fea57ff60", + "created_date": "2024-08-16 23:58:37.002000", + "last_modified_date": "2024-08-16 23:58:37.002000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "851bc748-2602-4f20-826f-a59a7087d11f", + "volume_id": null + }, + { + "id": "c1446de6-b76f-49d4-a685-c9369415b166", + "created_date": "2024-08-16 23:58:35.966000", + "last_modified_date": "2024-08-16 23:58:35.966000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", + "volume_id": null + }, + { + "id": "c1dc88c9-a523-4db6-b8cd-e51ad0cd8216", + "created_date": "2024-08-16 23:58:37.128000", + "last_modified_date": "2024-08-16 23:58:37.128000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "8", + "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", + "volume_id": null + }, + { + "id": "c1de66d3-9e35-4ecd-8e48-c66465149dcf", + "created_date": "2024-08-16 23:58:36.972000", + "last_modified_date": "2024-08-16 23:58:36.972000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "9", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "c254525b-8240-4580-a555-97089523f61c", + "created_date": "2024-08-16 23:58:36.342000", + "last_modified_date": "2024-08-16 23:58:36.342000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", + "volume_id": null + }, + { + "id": "c2713db3-c3af-47e9-aa46-7065842c1fc3", + "created_date": "2024-08-16 23:58:36.375000", + "last_modified_date": "2024-08-16 23:58:36.375000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "e048cec2-52b9-48f8-9975-cfe17ed85aae", + "volume_id": null + }, + { + "id": "c3089517-03e0-4b76-97c1-2931a84d180b", + "created_date": "2024-08-16 23:58:36.561000", + "last_modified_date": "2024-08-16 23:58:36.561000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "100b82bf-c134-40d9-bcc2-a8b345030b8d", + "volume_id": null + }, + { + "id": "c352f3c2-a152-41d7-894b-06e36900af02", + "created_date": "2024-08-16 23:58:36.577000", + "last_modified_date": "2024-08-16 23:58:36.577000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "0095a769-e5e2-441e-8a38-f98743ee9529", + "volume_id": null + }, + { + "id": "c356c1d8-896d-4da1-9dfa-6219a637e99d", + "created_date": "2024-08-16 23:58:35.751000", + "last_modified_date": "2024-08-16 23:58:35.751000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", + "volume_id": null + }, + { + "id": "c3613366-80bd-443b-b86b-d939750af351", + "created_date": "2024-08-16 23:58:35.667000", + "last_modified_date": "2024-08-16 23:58:35.667000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", + "volume_id": null + }, + { + "id": "c37b1411-1c5a-46f2-b35d-4b8e92496a2c", + "created_date": "2024-08-16 23:58:35.997000", + "last_modified_date": "2024-08-16 23:58:35.997000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", + "volume_id": null + }, + { + "id": "c3913ad6-ef63-4d27-b588-5bcf16ee5807", + "created_date": "2024-08-16 23:58:36.692000", + "last_modified_date": "2024-08-16 23:58:36.692000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "14", + "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", + "volume_id": null + }, + { + "id": "c3efa06e-6516-4f9f-b07e-bf3c0e140852", + "created_date": "2024-08-16 23:58:36.498000", + "last_modified_date": "2024-08-16 23:58:36.498000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "11", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "c4064467-0a2a-4422-bcaf-d25b081d9fe0", + "created_date": "2024-08-16 23:58:36.085000", + "last_modified_date": "2024-08-16 23:58:36.085000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "11bc20d5-bfeb-4825-9e0f-3d6954020b07", + "volume_id": null + }, + { + "id": "c41ea90c-a8ce-430c-8851-ec9b24e5ae82", + "created_date": "2024-08-16 23:58:36.328000", + "last_modified_date": "2024-08-16 23:58:36.328000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "7c110b15-bbfc-472e-830b-b8db6ddb274e", + "volume_id": null + }, + { + "id": "c531bbba-d203-4c4a-bc00-06a202cac746", + "created_date": "2024-08-16 23:58:37.086000", + "last_modified_date": "2024-08-16 23:58:37.086000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "b4c866cb-9461-4aa4-bc3b-ef3e63848775", + "volume_id": null + }, + { + "id": "c5a771bb-4d01-4174-acb7-4b05c66965e1", + "created_date": "2024-08-16 23:58:36.990000", + "last_modified_date": "2024-08-16 23:58:36.990000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "20", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "c5b0f36b-c457-47bf-914f-7826bdad22b5", + "created_date": "2024-08-16 23:58:37.008000", + "last_modified_date": "2024-08-16 23:58:37.008000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "505", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "c5b5a81f-dff7-4d55-a215-b532f2ec90f5", + "created_date": "2024-08-16 23:58:36.896000", + "last_modified_date": "2024-08-16 23:58:36.896000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "0fd9969e-8e1f-4f42-94ac-fab4d2d614c2", + "volume_id": null + }, + { + "id": "c5c73a82-b5a5-43fb-827b-b8e8eea8f49b", + "created_date": "2024-08-16 23:58:35.695000", + "last_modified_date": "2024-08-16 23:58:35.695000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "c69cc23d-5645-436f-a800-d577cbd64662", + "created_date": "2024-08-16 23:58:36.445000", + "last_modified_date": "2024-08-16 23:58:36.445000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "20", + "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", + "volume_id": null + }, + { + "id": "c9641ed7-7ab3-4c0f-8f1b-daecccfc4088", + "created_date": "2024-08-16 23:58:36.704000", + "last_modified_date": "2024-08-16 23:58:36.704000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "a2409ef1-82c3-45d1-9c65-b8806a31e525", + "volume_id": null + }, + { + "id": "c984ccce-35fe-467f-b30a-c18c9df93aa7", + "created_date": "2024-08-16 23:58:37.044000", + "last_modified_date": "2024-08-16 23:58:37.044000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "526", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "c9d86579-7243-4bc7-8381-9a33a6770e9b", + "created_date": "2024-08-16 23:58:36.323000", + "last_modified_date": "2024-08-16 23:58:36.323000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "7c110b15-bbfc-472e-830b-b8db6ddb274e", + "volume_id": null + }, + { + "id": "c9da40fe-00d5-4291-88c2-0987e5f45a3e", + "created_date": "2024-08-16 23:58:37.103000", + "last_modified_date": "2024-08-16 23:58:37.103000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "07574acf-8f77-4da7-87fd-c49f40536baf", + "volume_id": null + }, + { + "id": "ca026a61-30bd-452c-9943-610b20a263e9", + "created_date": "2024-08-16 23:58:36.517000", + "last_modified_date": "2024-08-16 23:58:36.517000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "22", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "ca4c8f3f-1e50-4d25-8090-46ba93661c35", + "created_date": "2024-08-16 23:58:36.325000", + "last_modified_date": "2024-08-16 23:58:36.325000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "7c110b15-bbfc-472e-830b-b8db6ddb274e", + "volume_id": null + }, + { + "id": "ca9ac5fe-0e69-46a0-a3e0-570e2267958a", + "created_date": "2024-08-16 23:58:36.332000", + "last_modified_date": "2024-08-16 23:58:36.332000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", + "volume_id": null + }, + { + "id": "cb347040-e464-44db-b9d4-9a3ae66cdef5", + "created_date": "2024-08-16 23:58:36.642000", + "last_modified_date": "2024-08-16 23:58:36.642000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "103", + "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", + "volume_id": null + }, + { + "id": "cb68308e-5c1f-4476-ab73-c95066047ec8", + "created_date": "2024-08-16 23:58:36.075000", + "last_modified_date": "2024-08-16 23:58:36.075000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "13", + "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", + "volume_id": null + }, + { + "id": "cb88d324-ef6e-42e1-8c03-a6e0b6b78f44", + "created_date": "2024-08-16 23:58:36.506000", + "last_modified_date": "2024-08-16 23:58:36.506000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "16", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "cbff57c0-e21f-4a69-ae77-45aa7f13e63e", + "created_date": "2024-08-16 23:58:37.127000", + "last_modified_date": "2024-08-16 23:58:37.127000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", + "volume_id": null + }, + { + "id": "cc01f0d9-377f-4393-b52c-312edacbeb56", + "created_date": "2024-08-16 23:58:36.346000", + "last_modified_date": "2024-08-16 23:58:36.346000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "9", + "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", + "volume_id": null + }, + { + "id": "cc86eb80-56c7-4865-8811-615a566fb8cb", + "created_date": "2024-08-16 23:58:36.169000", + "last_modified_date": "2024-08-16 23:58:36.169000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", + "volume_id": null + }, + { + "id": "ccda3fa2-248f-40a5-9d1f-2ddd9972772c", + "created_date": "2024-08-16 23:58:36.758000", + "last_modified_date": "2024-08-16 23:58:36.758000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", + "volume_id": null + }, + { + "id": "cd04f942-1be1-4d6f-9e0b-2c6144b9a7ca", + "created_date": "2024-08-16 23:58:36.564000", + "last_modified_date": "2024-08-16 23:58:36.564000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "45bd0c8a-845f-4ab3-9281-c31e5a3d4472", + "volume_id": null + }, + { + "id": "cdc011cf-2fab-4521-adee-06bbcaf0f583", + "created_date": "2024-08-16 23:58:37.133000", + "last_modified_date": "2024-08-16 23:58:37.133000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "11", + "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", + "volume_id": null + }, + { + "id": "cdf07ebc-d6e0-4667-8632-678c18b3b866", + "created_date": "2024-08-16 23:58:36.233000", + "last_modified_date": "2024-08-16 23:58:36.233000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "ff81bcf6-b368-4264-872c-544e85ec80e8", + "volume_id": null + }, + { + "id": "cdfc9a86-33e6-4554-9a60-a03023a0d58a", + "created_date": "2024-08-16 23:58:36.029000", + "last_modified_date": "2024-08-16 23:58:36.029000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "18", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", + "volume_id": null + }, + { + "id": "ce348d2c-d1e3-4d3c-9b14-4fe917891c10", + "created_date": "2024-08-16 23:58:36.503000", + "last_modified_date": "2024-08-16 23:58:36.503000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "14", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "ce4a5c02-2ce1-4264-8ff1-526544f14a9e", + "created_date": "2024-08-16 23:58:36.196000", + "last_modified_date": "2024-08-16 23:58:36.196000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "efc52177-93a0-4e69-a76f-2c9049ff3967", + "volume_id": null + }, + { + "id": "cefd3450-38a0-4b7c-b917-8931c03d85fc", + "created_date": "2024-08-16 23:58:36.584000", + "last_modified_date": "2024-08-16 23:58:36.584000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "8955b2a3-84d3-4b55-a1f1-d45193600861", + "volume_id": null + }, + { + "id": "cfcf2d8c-41bb-4783-9d04-45109eed329d", + "created_date": "2024-08-16 23:58:36.238000", + "last_modified_date": "2024-08-16 23:58:36.238000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "ff81bcf6-b368-4264-872c-544e85ec80e8", + "volume_id": null + }, + { + "id": "d134ea40-2790-4960-9c6b-c99ba4e45fbb", + "created_date": "2024-08-16 23:58:35.924000", + "last_modified_date": "2024-08-16 23:58:35.924000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", + "volume_id": null + }, + { + "id": "d139d42d-b36d-4d38-8f48-6f0ae7ba4bff", + "created_date": "2024-08-16 23:58:36.418000", + "last_modified_date": "2024-08-16 23:58:36.418000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "010ffe6b-54e4-42d7-86b6-581e680c35ad", + "volume_id": null + }, + { + "id": "d1a924cc-08b3-4465-a9b3-be10958aadb4", + "created_date": "2024-08-16 23:58:36.303000", + "last_modified_date": "2024-08-16 23:58:36.303000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "13281ef7-5945-49a9-b8f1-c5e8548c18ec", + "volume_id": null + }, + { + "id": "d1b48b90-abb2-445f-823d-e0c721355abc", + "created_date": "2024-08-16 23:58:36.645000", + "last_modified_date": "2024-08-16 23:58:36.645000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "105", + "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", + "volume_id": null + }, + { + "id": "d1e7182b-a0e7-4d6d-99ce-80ea1a0934ae", + "created_date": "2024-08-16 23:58:36.471000", + "last_modified_date": "2024-08-16 23:58:36.471000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "31", + "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", + "volume_id": null + }, + { + "id": "d22f3e1e-4fba-43b3-b4f1-d8d1e14657c4", + "created_date": "2024-08-16 23:58:36.100000", + "last_modified_date": "2024-08-16 23:58:36.100000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "9b924cdc-8959-41e0-a84d-f3e61bbeac44", + "volume_id": null + }, + { + "id": "d26715ca-db32-4f78-bb34-bae678419439", + "created_date": "2024-08-16 23:58:36.918000", + "last_modified_date": "2024-08-16 23:58:36.918000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "df47cdb1-0b41-4baa-83ce-fcfbb1c2bd51", + "volume_id": null + }, + { + "id": "d3425ed8-c9a4-478f-972e-d40d219a75e3", + "created_date": "2024-08-16 23:58:36.928000", + "last_modified_date": "2024-08-16 23:58:36.928000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "df47cdb1-0b41-4baa-83ce-fcfbb1c2bd51", + "volume_id": null + }, + { + "id": "d374a493-e80b-4bce-91f5-85abce983585", + "created_date": "2024-08-16 23:58:36.200000", + "last_modified_date": "2024-08-16 23:58:36.200000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", + "volume_id": null + }, + { + "id": "d3dcf1ce-52a1-41eb-bbfe-691531a197d9", + "created_date": "2024-08-16 23:58:36.062000", + "last_modified_date": "2024-08-16 23:58:36.062000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "8", + "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", + "volume_id": null + }, + { + "id": "d4655e35-d8e9-437f-836f-cbe52f5c2060", + "created_date": "2024-08-16 23:58:35.798000", + "last_modified_date": "2024-08-16 23:58:35.798000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "b51c738d-8ba1-4451-8f89-f49c1968ac42", + "volume_id": null + }, + { + "id": "d49ae93e-c0da-44b0-9938-5524cba4a1cc", + "created_date": "2024-08-16 23:58:37.019000", + "last_modified_date": "2024-08-16 23:58:37.019000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "512", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "d4af7d68-9f41-4926-a541-18c7d76594fa", + "created_date": "2024-08-16 23:58:36.350000", + "last_modified_date": "2024-08-16 23:58:36.350000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "11", + "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", + "volume_id": null + }, + { + "id": "d4f99702-dccc-42d7-bd28-058393cd83f8", + "created_date": "2024-08-16 23:58:37.076000", + "last_modified_date": "2024-08-16 23:58:37.076000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "231e65eb-eecb-4946-a643-6184b767e321", + "volume_id": null + }, + { + "id": "d53f7493-72e1-45d5-a573-c45ae912dca6", + "created_date": "2024-08-16 23:58:36.623000", + "last_modified_date": "2024-08-16 23:58:36.623000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "1d9440ba-202a-4c4e-8425-8561606c1c3e", + "volume_id": null + }, + { + "id": "d553ba7e-cb3b-4fb1-b1be-24fea1622edc", + "created_date": "2024-08-16 23:58:36.175000", + "last_modified_date": "2024-08-16 23:58:36.175000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", + "volume_id": null + }, + { + "id": "d569785a-015b-4e57-912d-a9e6ea94d5e8", + "created_date": "2024-08-16 23:58:35.987000", + "last_modified_date": "2024-08-16 23:58:35.987000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "12", + "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", + "volume_id": null + }, + { + "id": "d570c5aa-6631-4fd1-a727-4984f6a4f17f", + "created_date": "2024-08-16 23:58:36.974000", + "last_modified_date": "2024-08-16 23:58:36.974000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "10", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "d59bb10b-fe68-400c-b226-2efdc1f7a5dd", + "created_date": "2024-08-16 23:58:36.719000", + "last_modified_date": "2024-08-16 23:58:36.719000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "e1ff1410-ac3a-4ba5-8503-1fab529e50c0", + "volume_id": null + }, + { + "id": "d61c1d1a-8834-4e3e-a8d3-ecbbb6a6dd46", + "created_date": "2024-08-16 23:58:36.193000", + "last_modified_date": "2024-08-16 23:58:36.193000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "efc52177-93a0-4e69-a76f-2c9049ff3967", + "volume_id": null + }, + { + "id": "d6593b47-10e2-4bf7-8c65-efae8bdc1ce9", + "created_date": "2024-08-16 23:58:35.781000", + "last_modified_date": "2024-08-16 23:58:35.781000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "5a72a63d-8fbd-46a8-b201-9b6e035c782a", + "volume_id": null + }, + { + "id": "d676914f-d1cf-42f9-a552-49ce09f4bb37", + "created_date": "2024-08-16 23:58:36.024000", + "last_modified_date": "2024-08-16 23:58:36.024000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "15", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", + "volume_id": null + }, + { + "id": "d67961ad-d8bb-43ee-9a16-df132e255bb8", + "created_date": "2024-08-16 23:58:35.943000", + "last_modified_date": "2024-08-16 23:58:35.943000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "9", + "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", + "volume_id": null + }, + { + "id": "d6906f96-d8b3-428f-b006-c61a23cab7f0", + "created_date": "2024-08-16 23:58:37.047000", + "last_modified_date": "2024-08-16 23:58:37.047000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "a5987e5c-0245-484d-aeb8-4b5195800d66", + "volume_id": null + }, + { + "id": "d6908f9e-79e3-4e04-b3d0-33328fe13bc7", + "created_date": "2024-08-16 23:58:35.836000", + "last_modified_date": "2024-08-16 23:58:35.836000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "d6e1d653-5262-4f1a-9260-799e37bd7cd4", + "created_date": "2024-08-16 23:58:36.484000", + "last_modified_date": "2024-08-16 23:58:36.484000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "d7013d89-ac50-4094-8312-18dd3be11d07", + "created_date": "2024-08-16 23:58:35.634000", + "last_modified_date": "2024-08-16 23:58:35.634000", + "version": 0, + "in_stock": 0, + "is_read": 1, + "issue_number": "9", + "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", + "volume_id": null + }, + { + "id": "d70d8f9a-9501-4e07-af2b-519ebc2ca2aa", + "created_date": "2024-08-16 23:58:36.588000", + "last_modified_date": "2024-08-16 23:58:36.588000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "14", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "d78a1e50-9808-43c4-9c57-f167627ef699", + "created_date": "2024-08-16 23:58:36.725000", + "last_modified_date": "2024-08-16 23:58:36.725000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "25841b05-c246-4484-9ef2-71f8dcdb39ed", + "volume_id": null + }, + { + "id": "d850c042-b793-4a23-9391-1c5cfcc27011", + "created_date": "2024-08-16 23:58:37.011000", + "last_modified_date": "2024-08-16 23:58:37.011000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "507", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "d9cf1648-0326-4201-b687-64e3ed616118", + "created_date": "2024-08-16 23:58:36.891000", + "last_modified_date": "2024-08-16 23:58:36.891000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "80f818f1-3813-4bf9-9eb2-0a441799fa6d", + "volume_id": null + }, + { + "id": "db290641-7a7e-4349-a999-ade57a73842d", + "created_date": "2024-08-16 23:58:35.696000", + "last_modified_date": "2024-08-16 23:58:35.696000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "dbb71353-f0a6-4c13-b58f-d5b135d3956a", + "created_date": "2024-08-16 23:58:36.273000", + "last_modified_date": "2024-08-16 23:58:36.273000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "ef71fb0f-ecba-4d88-ae5f-90c81b0c4e18", + "volume_id": null + }, + { + "id": "dbbd1211-03ba-4770-b5af-fa99947509f7", + "created_date": "2024-08-16 23:58:36.248000", + "last_modified_date": "2024-08-16 23:58:36.248000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "639eed1d-3ccf-4bfa-a595-06b44a4e5b8f", + "volume_id": null + }, + { + "id": "dd067de8-d411-4abc-95ba-060821dc3bc8", + "created_date": "2024-08-16 23:58:36.154000", + "last_modified_date": "2024-08-16 23:58:36.154000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "5fc6600f-b005-4d6b-a1af-a17cc2701d81", + "volume_id": null + }, + { + "id": "dd11fe97-780e-42b9-a8a0-b7c30b71a0ea", + "created_date": "2024-08-16 23:58:36.613000", + "last_modified_date": "2024-08-16 23:58:36.613000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "29", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", + "volume_id": null + }, + { + "id": "dd2843ff-5c4a-455e-a6a1-9836b641baee", + "created_date": "2024-08-16 23:58:36.751000", + "last_modified_date": "2024-08-16 23:58:36.751000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "4f0e16a3-452f-43cd-9834-d0f4d2d5fca0", + "volume_id": null + }, + { + "id": "dd617f85-37ef-4d53-967c-8806c78a6e01", + "created_date": "2024-08-16 23:58:36.961000", + "last_modified_date": "2024-08-16 23:58:36.961000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "dd97ad26-b348-4a03-b5bf-e635295fa24c", + "created_date": "2024-08-16 23:58:36.505000", + "last_modified_date": "2024-08-16 23:58:36.505000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "15", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "de0b91de-6de5-489b-a192-73c095184887", + "created_date": "2024-08-16 23:58:36.491000", + "last_modified_date": "2024-08-16 23:58:36.491000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "ded7fec4-528e-44b2-9a8d-bd3221f9c2fc", + "created_date": "2024-08-16 23:58:36.897000", + "last_modified_date": "2024-08-16 23:58:36.897000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "799309bc-d8d9-4d44-9457-bae7f1e7fcbf", + "volume_id": null + }, + { + "id": "dee5fa1b-7eae-4a3c-bdb8-0704b102ff38", + "created_date": "2024-08-16 23:58:36.171000", + "last_modified_date": "2024-08-16 23:58:36.171000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", + "volume_id": null + }, + { + "id": "df0e1478-a0b2-41c0-bee3-b23143eb6590", + "created_date": "2024-08-16 23:58:36.823000", + "last_modified_date": "2024-08-16 23:58:36.823000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "c0a044dd-461b-459a-9962-b94ed0e8b38f", + "volume_id": null + }, + { + "id": "df67806e-4968-4abe-80f8-802e1cb43b6e", + "created_date": "2024-08-16 23:58:35.637000", + "last_modified_date": "2024-08-16 23:58:35.637000", + "version": 0, + "in_stock": 0, + "is_read": 1, + "issue_number": "10", + "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", + "volume_id": null + }, + { + "id": "df6c8680-36d4-459a-9e96-7ce14c4bf03d", + "created_date": "2024-08-16 23:58:35.784000", + "last_modified_date": "2024-08-16 23:58:35.784000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "5a72a63d-8fbd-46a8-b201-9b6e035c782a", + "volume_id": null + }, + { + "id": "dfde9a1b-2903-472c-8242-35ff9278b08e", + "created_date": "2024-08-16 23:58:35.833000", + "last_modified_date": "2024-08-16 23:58:35.833000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "e0171122-e40f-471d-a6c8-42bfce7c78d9", + "created_date": "2024-08-16 23:58:36.944000", + "last_modified_date": "2024-08-16 23:58:36.944000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "44", + "comic_id": "2a4b287e-4b05-4016-8eb4-21fc225be24d", + "volume_id": null + }, + { + "id": "e095447f-eaf5-4608-b1ac-176587174252", + "created_date": "2024-08-16 23:58:37.107000", + "last_modified_date": "2024-08-16 23:58:37.107000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "639f7e71-1012-49a6-bc3a-1ac7b1de3084", + "volume_id": null + }, + { + "id": "e0db2046-963a-4889-bc27-dbd28c0e5c54", + "created_date": "2024-08-16 23:58:36.236000", + "last_modified_date": "2024-08-16 23:58:36.236000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "ff81bcf6-b368-4264-872c-544e85ec80e8", + "volume_id": null + }, + { + "id": "e0fd5afe-9d21-4baf-bf93-23c628f8b246", + "created_date": "2024-08-16 23:58:36.362000", + "last_modified_date": "2024-08-16 23:58:36.362000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "c91ec109-0b4d-4fd6-995f-1a828958493f", + "volume_id": null + }, + { + "id": "e1125ccb-d440-4c85-8677-fd5347f74147", + "created_date": "2024-08-16 23:58:36.550000", + "last_modified_date": "2024-08-16 23:58:36.550000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "bf59f643-455e-4b60-95b8-719d55437474", + "volume_id": null + }, + { + "id": "e16418ed-65c3-4925-9bec-8bef8732ab52", + "created_date": "2024-08-16 23:58:36.698000", + "last_modified_date": "2024-08-16 23:58:36.698000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "152", + "comic_id": "cc96b25e-b827-4ff0-a94a-b82d30ca883c", + "volume_id": null + }, + { + "id": "e191bee5-85dc-43d4-9ade-d8c006a93dcc", + "created_date": "2024-08-16 23:58:35.807000", + "last_modified_date": "2024-08-16 23:58:35.807000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "89c5ea13-997a-4831-87cc-ee76ea05c71e", + "volume_id": null + }, + { + "id": "e1d76830-7277-48d9-b2af-c74f1248b5c5", + "created_date": "2024-08-16 23:58:35.632000", + "last_modified_date": "2024-08-16 23:58:35.632000", + "version": 0, + "in_stock": 0, + "is_read": 1, + "issue_number": "8", + "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", + "volume_id": null + }, + { + "id": "e21ce1d0-7349-4a93-943f-4fd41bbdd175", + "created_date": "2024-08-16 23:58:35.778000", + "last_modified_date": "2024-08-16 23:58:35.778000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "5a72a63d-8fbd-46a8-b201-9b6e035c782a", + "volume_id": null + }, + { + "id": "e305c4a5-37a4-41e4-8c58-b5b065e4fdf5", + "created_date": "2024-08-16 23:58:36.720000", + "last_modified_date": "2024-08-16 23:58:36.720000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "25841b05-c246-4484-9ef2-71f8dcdb39ed", + "volume_id": null + }, + { + "id": "e3728535-ffcd-47e0-ab3c-e26cb8d1f214", + "created_date": "2024-08-16 23:58:36.663000", + "last_modified_date": "2024-08-16 23:58:36.663000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "c9d4b26a-8431-4f68-8e7a-b61f2cf24176", + "volume_id": null + }, + { + "id": "e37d3491-8104-46e1-a3e2-f3f0e5bd648d", + "created_date": "2024-08-16 23:58:36.705000", + "last_modified_date": "2024-08-16 23:58:36.705000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "a2409ef1-82c3-45d1-9c65-b8806a31e525", + "volume_id": null + }, + { + "id": "e38f4fc3-7823-478d-a5bc-62dfe82e36d3", + "created_date": "2024-08-16 23:58:36.641000", + "last_modified_date": "2024-08-16 23:58:36.641000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "102", + "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", + "volume_id": null + }, + { + "id": "e3cb2b41-bba4-40a6-a8d9-b4ce1b79a4d6", + "created_date": "2024-08-16 23:58:36.485000", + "last_modified_date": "2024-08-16 23:58:36.485000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "e3dd5af3-1c7c-4391-971b-f13346957c00", + "created_date": "2024-08-16 23:58:36.480000", + "last_modified_date": "2024-08-16 23:58:36.480000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "35", + "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", + "volume_id": null + }, + { + "id": "e41501fa-cb58-4945-ad0e-1faf3be4bb68", + "created_date": "2024-08-16 23:58:36.838000", + "last_modified_date": "2024-08-16 23:58:36.838000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "9fcdd9a5-f1fb-4421-a352-9da8e2c12f81", + "volume_id": null + }, + { + "id": "e493d4c5-7d93-49a2-b8a0-9b9177ee91e3", + "created_date": "2024-08-16 23:58:36.195000", + "last_modified_date": "2024-08-16 23:58:36.195000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "efc52177-93a0-4e69-a76f-2c9049ff3967", + "volume_id": null + }, + { + "id": "e4cda530-52c5-4ead-9723-3c277c484cba", + "created_date": "2024-08-16 23:58:36.886000", + "last_modified_date": "2024-08-16 23:58:36.886000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "10", + "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", + "volume_id": null + }, + { + "id": "e54e71be-d5fe-4de7-9ad7-ebc3156b578c", + "created_date": "2024-08-16 23:58:36.624000", + "last_modified_date": "2024-08-16 23:58:36.624000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "1d9440ba-202a-4c4e-8425-8561606c1c3e", + "volume_id": null + }, + { + "id": "e5701dab-a038-4bee-9086-602c6848cc39", + "created_date": "2024-08-16 23:58:36.230000", + "last_modified_date": "2024-08-16 23:58:36.230000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "fe7fb34e-5ff2-4f6d-a649-0be19a368f46", + "volume_id": null + }, + { + "id": "e645cbc3-bdb2-4278-bcc2-d4412b9de60f", + "created_date": "2024-08-16 23:58:35.653000", + "last_modified_date": "2024-08-16 23:58:35.653000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", + "volume_id": null + }, + { + "id": "e6649305-1c25-46c2-a030-e8b067627c0f", + "created_date": "2024-08-16 23:58:36.410000", + "last_modified_date": "2024-08-16 23:58:36.410000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "48", + "comic_id": "57965b27-1330-4921-8c0b-4b09ee06084f", + "volume_id": null + }, + { + "id": "e66f1ea0-65ed-4f8a-8488-f3aec2dca7b0", + "created_date": "2024-08-16 23:58:36.330000", + "last_modified_date": "2024-08-16 23:58:36.330000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "7c110b15-bbfc-472e-830b-b8db6ddb274e", + "volume_id": null + }, + { + "id": "e71fd5dc-15b3-442b-8241-8adfe9abcde3", + "created_date": "2024-08-16 23:58:36.832000", + "last_modified_date": "2024-08-16 23:58:36.832000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "8e293af3-05c6-4dcc-9cbf-b87512ec975b", + "volume_id": null + }, + { + "id": "e7920076-289a-4ebf-8ca7-4b54c04c7ed5", + "created_date": "2024-08-16 23:58:35.611000", + "last_modified_date": "2024-08-16 23:58:35.611000", + "version": 0, + "in_stock": 0, + "is_read": 1, + "issue_number": "1", + "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", + "volume_id": null + }, + { + "id": "e7ff7168-4229-4456-ad2a-b2ef5420bc2c", + "created_date": "2024-08-16 23:58:35.851000", + "last_modified_date": "2024-08-16 23:58:35.851000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "11", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "e893ea6d-50c8-4d53-963a-920b4bd41a9d", + "created_date": "2024-08-16 23:58:37.021000", + "last_modified_date": "2024-08-16 23:58:37.021000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "513", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "e8a565ce-da7c-43b3-b433-a6bebdac4641", + "created_date": "2024-08-16 23:58:36.215000", + "last_modified_date": "2024-08-16 23:58:36.215000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "10", + "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", + "volume_id": null + }, + { + "id": "e9501fdb-6b64-459a-a37d-452b7b528be7", + "created_date": "2024-08-16 23:58:36.900000", + "last_modified_date": "2024-08-16 23:58:36.900000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "799309bc-d8d9-4d44-9457-bae7f1e7fcbf", + "volume_id": null + }, + { + "id": "e95a3246-4e9b-414a-8f22-3e727535b6a4", + "created_date": "2024-08-16 23:58:36.937000", + "last_modified_date": "2024-08-16 23:58:36.937000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "40", + "comic_id": "2a4b287e-4b05-4016-8eb4-21fc225be24d", + "volume_id": null + }, + { + "id": "e96fa441-7085-4c40-acc8-871f947868f4", + "created_date": "2024-08-16 23:58:36.479000", + "last_modified_date": "2024-08-16 23:58:36.479000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "34", + "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", + "volume_id": null + }, + { + "id": "e979103a-3655-43a6-8047-af3967fe3273", + "created_date": "2024-08-16 23:58:36.367000", + "last_modified_date": "2024-08-16 23:58:36.367000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "0", + "comic_id": "b6713944-8d2d-4153-8f16-fe94cc4ee119", + "volume_id": null + }, + { + "id": "e9964166-fd4c-41bf-b3e2-5f5d0fdd9365", + "created_date": "2024-08-16 23:58:35.888000", + "last_modified_date": "2024-08-16 23:58:35.888000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "23", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "e9a24aeb-e5cc-458a-bd9b-2379ae331df7", + "created_date": "2024-08-16 23:58:36.953000", + "last_modified_date": "2024-08-16 23:58:36.953000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "d33657a6-0ab7-4177-b5b3-4a0c9d358d03", + "volume_id": null + }, + { + "id": "ea29f973-91a6-4056-a4c4-3851b8552202", + "created_date": "2024-08-16 23:58:36.979000", + "last_modified_date": "2024-08-16 23:58:36.979000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "13", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "eaba8699-b092-460d-be8b-326222e02ec8", + "created_date": "2024-08-16 23:58:36.556000", + "last_modified_date": "2024-08-16 23:58:36.556000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "100b82bf-c134-40d9-bcc2-a8b345030b8d", + "volume_id": null + }, + { + "id": "eb3c9af0-7c94-4020-83ea-40e923e767ae", + "created_date": "2024-08-16 23:58:36.545000", + "last_modified_date": "2024-08-16 23:58:36.545000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "bf59f643-455e-4b60-95b8-719d55437474", + "volume_id": null + }, + { + "id": "ebd68581-fe3c-40ce-bd1a-c64960a65d78", + "created_date": "2024-08-16 23:58:36.913000", + "last_modified_date": "2024-08-16 23:58:36.913000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "a08aef89-634c-494c-9def-73ff7e416464", + "volume_id": null + }, + { + "id": "ebebecc2-b31c-43ee-9d7b-30048356b132", + "created_date": "2024-08-16 23:58:36.427000", + "last_modified_date": "2024-08-16 23:58:36.427000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "83", + "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", + "volume_id": null + }, + { + "id": "ede115b0-ae5e-4fd5-ba19-5e2ad56ddb8b", + "created_date": "2024-08-16 23:58:36.684000", + "last_modified_date": "2024-08-16 23:58:36.684000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "10", + "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", + "volume_id": null + }, + { + "id": "ee804dc0-58b8-46b3-a9d7-904030506bdc", + "created_date": "2024-08-16 23:58:36.441000", + "last_modified_date": "2024-08-16 23:58:36.441000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "92", + "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", + "volume_id": null + }, + { + "id": "eeb2ad49-a1c3-4d41-b63f-8e8ae7b271fe", + "created_date": "2024-08-16 23:58:36.148000", + "last_modified_date": "2024-08-16 23:58:36.148000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "7", + "comic_id": "c0543c9f-d712-4dce-9b59-2bf73b800b31", + "volume_id": null + }, + { + "id": "eeb4499e-7a67-469f-9672-44081a35f651", + "created_date": "2024-08-16 23:58:36.152000", + "last_modified_date": "2024-08-16 23:58:36.152000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "5fc6600f-b005-4d6b-a1af-a17cc2701d81", + "volume_id": null + }, + { + "id": "eed2495c-cf50-47d2-9ceb-23ebf4174b48", + "created_date": "2024-08-16 23:58:36.796000", + "last_modified_date": "2024-08-16 23:58:36.796000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "10", + "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", + "volume_id": null + }, + { + "id": "ef0e647d-cfc2-4fbe-a09e-5ea454b381f2", + "created_date": "2024-08-16 23:58:36.548000", + "last_modified_date": "2024-08-16 23:58:36.548000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "bf59f643-455e-4b60-95b8-719d55437474", + "volume_id": null + }, + { + "id": "f04e696e-674c-4dcd-ab12-bb8897b553dd", + "created_date": "2024-08-16 23:58:36.396000", + "last_modified_date": "2024-08-16 23:58:36.396000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "214", + "comic_id": "d24801e2-fbfe-4497-873f-4d8edb182ae4", + "volume_id": null + }, + { + "id": "f058b0f7-a0e1-4aae-a4e3-2f4d0ff64435", + "created_date": "2024-08-16 23:58:35.786000", + "last_modified_date": "2024-08-16 23:58:35.786000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "5a72a63d-8fbd-46a8-b201-9b6e035c782a", + "volume_id": null + }, + { + "id": "f05f4330-6428-426d-ab61-86ca69054497", + "created_date": "2024-08-16 23:58:36.831000", + "last_modified_date": "2024-08-16 23:58:36.831000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "8e293af3-05c6-4dcc-9cbf-b87512ec975b", + "volume_id": null + }, + { + "id": "f104ab76-8013-458e-aff4-6390ba9c15db", + "created_date": "2024-08-16 23:58:37.114000", + "last_modified_date": "2024-08-16 23:58:37.114000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "ea903b99-4032-4b4e-add4-177e051733a8", + "volume_id": null + }, + { + "id": "f1327ef4-975d-44c3-8b46-c5640789fa0e", + "created_date": "2024-08-16 23:58:36.539000", + "last_modified_date": "2024-08-16 23:58:36.539000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "f4cb5b24-00ea-4249-9d09-45432c168b8f", + "volume_id": null + }, + { + "id": "f14a6885-22b7-4495-bb22-3f95d64e9a97", + "created_date": "2024-08-16 23:58:36.967000", + "last_modified_date": "2024-08-16 23:58:36.967000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "f1622875-aa1c-4d96-91c1-7aaabf474394", + "created_date": "2024-08-16 23:58:36.309000", + "last_modified_date": "2024-08-16 23:58:36.309000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "60deca87-7b2a-4412-855f-01a6ccaeea56", + "volume_id": null + }, + { + "id": "f1e0321f-629b-49dc-bb92-dfcf34814e9e", + "created_date": "2024-08-16 23:58:36.713000", + "last_modified_date": "2024-08-16 23:58:36.713000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "e1ff1410-ac3a-4ba5-8503-1fab529e50c0", + "volume_id": null + }, + { + "id": "f1fe0246-06f5-4f06-b4a4-83bb823f775e", + "created_date": "2024-08-16 23:58:35.994000", + "last_modified_date": "2024-08-16 23:58:35.994000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", + "volume_id": null + }, + { + "id": "f2596e0c-7b20-47f8-97e8-5976429ddae0", + "created_date": "2024-08-16 23:58:36.095000", + "last_modified_date": "2024-08-16 23:58:36.095000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "46d02138-9fb9-4fbe-9928-d73c0faf2a4e", + "volume_id": null + }, + { + "id": "f3107ebd-45e5-43b8-a126-70f2c068ea61", + "created_date": "2024-08-16 23:58:36.221000", + "last_modified_date": "2024-08-16 23:58:36.221000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "12", + "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", + "volume_id": null + }, + { + "id": "f319368c-010d-4084-946a-f1e508a50a40", + "created_date": "2024-08-16 23:58:35.715000", + "last_modified_date": "2024-08-16 23:58:35.715000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "14", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "f3613c94-48a1-4e08-b4a7-fef7f3760656", + "created_date": "2024-08-16 23:58:35.955000", + "last_modified_date": "2024-08-16 23:58:35.955000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "13", + "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", + "volume_id": null + }, + { + "id": "f40f0239-9612-4fed-8563-0da590ab4ef7", + "created_date": "2024-08-16 23:58:36.554000", + "last_modified_date": "2024-08-16 23:58:36.554000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "c686b9c2-abdc-4476-a530-ee85ce221a5d", + "volume_id": null + }, + { + "id": "f4b58c38-4096-4851-9557-9ef709e6d702", + "created_date": "2024-08-16 23:58:36.122000", + "last_modified_date": "2024-08-16 23:58:36.122000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "17b50be7-aca7-446e-8729-3706f636d29d", + "volume_id": null + }, + { + "id": "f51be125-6e35-43d6-8bc6-a569bcd599eb", + "created_date": "2024-08-16 23:58:36.114000", + "last_modified_date": "2024-08-16 23:58:36.114000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "0f0bfd13-f6f0-436c-ad74-5e40ea6a0cf8", + "volume_id": null + }, + { + "id": "f579c1aa-a23c-4892-9eba-d5921ddad67e", + "created_date": "2024-08-16 23:58:35.890000", + "last_modified_date": "2024-08-16 23:58:35.890000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "24", + "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", + "volume_id": null + }, + { + "id": "f5a56360-bea8-4b1a-9226-809efbd3b03b", + "created_date": "2024-08-16 23:58:35.647000", + "last_modified_date": "2024-08-16 23:58:35.647000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", + "volume_id": null + }, + { + "id": "f5b790f6-34e4-4751-8c5d-b4d667a4b289", + "created_date": "2024-08-16 23:58:36.732000", + "last_modified_date": "2024-08-16 23:58:36.732000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "fe69cf80-946a-45d6-9fba-55ebfc0f038c", + "volume_id": null + }, + { + "id": "f5f81308-6f74-4901-a601-3afb61e20138", + "created_date": "2024-08-16 23:58:36.125000", + "last_modified_date": "2024-08-16 23:58:36.125000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "f3231681-cd2b-4ff9-bfa2-5d8f631bee4d", + "volume_id": null + }, + { + "id": "f600f71a-17e0-41c0-a701-5d8df520b499", + "created_date": "2024-08-16 23:58:36.772000", + "last_modified_date": "2024-08-16 23:58:36.772000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "10", + "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", + "volume_id": null + }, + { + "id": "f606d273-1a00-42fa-bf79-db3a162dfb3a", + "created_date": "2024-08-16 23:58:36.717000", + "last_modified_date": "2024-08-16 23:58:36.717000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "e1ff1410-ac3a-4ba5-8503-1fab529e50c0", + "volume_id": null + }, + { + "id": "f609bb6d-55ed-4c93-8ea7-818f3a16c283", + "created_date": "2024-08-16 23:58:36.336000", + "last_modified_date": "2024-08-16 23:58:36.336000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", + "volume_id": null + }, + { + "id": "f62a7bd0-3c5f-4460-8a99-9d6ac10d2bfb", + "created_date": "2024-08-16 23:58:36.267000", + "last_modified_date": "2024-08-16 23:58:36.267000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "5d0fd720-7875-4f9f-86eb-07ee0723908f", + "volume_id": null + }, + { + "id": "f692a0e8-64bd-4f73-af9c-8549f13f9aa4", + "created_date": "2024-08-16 23:58:36.815000", + "last_modified_date": "2024-08-16 23:58:36.815000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "12802b17-452a-4533-a7ab-fd94f19be123", + "volume_id": null + }, + { + "id": "f6aebd74-9c70-46fb-b820-07ed07d7e823", + "created_date": "2024-08-16 23:58:37.113000", + "last_modified_date": "2024-08-16 23:58:37.113000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "2ab84282-d7f5-43ef-af41-df0f756a62f7", + "volume_id": null + }, + { + "id": "f6bdfb93-f196-4f01-a112-c80d9b93dde5", + "created_date": "2024-08-16 23:58:36.963000", + "last_modified_date": "2024-08-16 23:58:36.963000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "f72781ea-72f2-4f04-98b3-4f47172ec908", + "created_date": "2024-08-16 23:58:36.801000", + "last_modified_date": "2024-08-16 23:58:36.801000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "13", + "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", + "volume_id": null + }, + { + "id": "f769e448-19d1-4c38-89c3-e6ffb9bf340f", + "created_date": "2024-08-16 23:58:36.955000", + "last_modified_date": "2024-08-16 23:58:36.955000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "d33657a6-0ab7-4177-b5b3-4a0c9d358d03", + "volume_id": null + }, + { + "id": "f799357e-37ab-456b-a8e0-8e9758fe67b7", + "created_date": "2024-08-16 23:58:36.639000", + "last_modified_date": "2024-08-16 23:58:36.639000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "101", + "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", + "volume_id": null + }, + { + "id": "f7c25b8a-2fbf-4c14-b393-d5b9baf3fb0e", + "created_date": "2024-08-16 23:58:37.099000", + "last_modified_date": "2024-08-16 23:58:37.099000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "07574acf-8f77-4da7-87fd-c49f40536baf", + "volume_id": null + }, + { + "id": "f8566ced-aeec-47b3-9571-af8e406f0249", + "created_date": "2024-08-16 23:58:36.186000", + "last_modified_date": "2024-08-16 23:58:36.186000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "10", + "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", + "volume_id": null + }, + { + "id": "f883f05f-b815-4c74-bdca-12af771eef44", + "created_date": "2024-08-16 23:58:36.301000", + "last_modified_date": "2024-08-16 23:58:36.301000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "13281ef7-5945-49a9-b8f1-c5e8548c18ec", + "volume_id": null + }, + { + "id": "f8e1a631-262a-455c-9efc-cfdde559a123", + "created_date": "2024-08-16 23:58:36.057000", + "last_modified_date": "2024-08-16 23:58:36.057000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "6", + "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", + "volume_id": null + }, + { + "id": "f9159a59-34a0-4562-aef3-47eb7272fc77", + "created_date": "2024-08-16 23:58:36.083000", + "last_modified_date": "2024-08-16 23:58:36.083000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "11bc20d5-bfeb-4825-9e0f-3d6954020b07", + "volume_id": null + }, + { + "id": "f93c91e5-6f6f-4668-aaad-81bab3eb7253", + "created_date": "2024-08-16 23:58:35.982000", + "last_modified_date": "2024-08-16 23:58:35.982000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "10", + "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", + "volume_id": null + }, + { + "id": "f9481e79-639f-4b55-84b0-d4c49656792c", + "created_date": "2024-08-16 23:58:36.311000", + "last_modified_date": "2024-08-16 23:58:36.311000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "60deca87-7b2a-4412-855f-01a6ccaeea56", + "volume_id": null + }, + { + "id": "f978ee04-9b37-4d3f-8ce5-4c69a9b014e3", + "created_date": "2024-08-16 23:58:36.735000", + "last_modified_date": "2024-08-16 23:58:36.735000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "8690631a-e99c-44be-887c-8fd2bb222ee1", + "volume_id": null + }, + { + "id": "f9dd23f9-aa3b-4667-824e-eea1915060b7", + "created_date": "2024-08-16 23:58:36.451000", + "last_modified_date": "2024-08-16 23:58:36.451000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "23", + "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", + "volume_id": null + }, + { + "id": "f9f1c40e-e163-4691-ac2f-9a1ae219cb82", + "created_date": "2024-08-16 23:58:36.578000", + "last_modified_date": "2024-08-16 23:58:36.578000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "0095a769-e5e2-441e-8a38-f98743ee9529", + "volume_id": null + }, + { + "id": "fa05766f-3834-4e94-8598-f55ed2db08b4", + "created_date": "2024-08-16 23:58:36.637000", + "last_modified_date": "2024-08-16 23:58:36.637000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "100", + "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", + "volume_id": null + }, + { + "id": "fa5a93d9-c292-44ce-8be8-507d88198c7f", + "created_date": "2024-08-16 23:58:35.699000", + "last_modified_date": "2024-08-16 23:58:35.699000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "8", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "fa7aec1c-dbbf-4ea0-a99e-539d98464f85", + "created_date": "2024-08-16 23:58:36.235000", + "last_modified_date": "2024-08-16 23:58:36.235000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "ff81bcf6-b368-4264-872c-544e85ec80e8", + "volume_id": null + }, + { + "id": "fb104202-ef2a-4a4a-af38-8660e9331900", + "created_date": "2024-08-16 23:58:36.559000", + "last_modified_date": "2024-08-16 23:58:36.559000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "100b82bf-c134-40d9-bcc2-a8b345030b8d", + "volume_id": null + }, + { + "id": "fb5134d2-522c-4971-a333-831a33782e00", + "created_date": "2024-08-16 23:58:36.088000", + "last_modified_date": "2024-08-16 23:58:36.088000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "11bc20d5-bfeb-4825-9e0f-3d6954020b07", + "volume_id": null + }, + { + "id": "fc4f9cd5-6666-4576-9faa-642271aff2b9", + "created_date": "2024-08-16 23:58:37.016000", + "last_modified_date": "2024-08-16 23:58:37.016000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "510", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", + "volume_id": null + }, + { + "id": "fd3bf188-4b5b-4996-9f9a-29c3aa04df36", + "created_date": "2024-08-16 23:58:36.798000", + "last_modified_date": "2024-08-16 23:58:36.798000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "11", + "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", + "volume_id": null + }, + { + "id": "fd50d8e7-31c3-4dd1-bfc3-62dc55b23005", + "created_date": "2024-08-16 23:58:36.374000", + "last_modified_date": "2024-08-16 23:58:36.374000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "b6713944-8d2d-4153-8f16-fe94cc4ee119", + "volume_id": null + }, + { + "id": "fdabc2b4-b90c-49c2-9175-ba255c3a4a59", + "created_date": "2024-08-16 23:58:36.843000", + "last_modified_date": "2024-08-16 23:58:36.843000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", + "volume_id": null + }, + { + "id": "fdcaf3b8-a458-46f8-a3b6-560dd94e44aa", + "created_date": "2024-08-16 23:58:36.985000", + "last_modified_date": "2024-08-16 23:58:36.985000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "17", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "fdf07c29-e365-459e-8842-d8175ea5b810", + "created_date": "2024-08-16 23:58:36.035000", + "last_modified_date": "2024-08-16 23:58:36.035000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "bf23d317-f5b6-4cf8-8a05-4397888a82c9", + "volume_id": null + }, + { + "id": "fe0213ea-65a4-4995-ad65-a37244c0ce59", + "created_date": "2024-08-16 23:58:36.514000", + "last_modified_date": "2024-08-16 23:58:36.514000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "20", + "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", + "volume_id": null + }, + { + "id": "fe27df42-d815-4f43-b764-37b6ad2b723a", + "created_date": "2024-08-16 23:58:35.971000", + "last_modified_date": "2024-08-16 23:58:35.971000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "4", + "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", + "volume_id": null + }, + { + "id": "fe38f460-f857-435c-aa88-7bc58abe59fb", + "created_date": "2024-08-16 23:58:36.965000", + "last_modified_date": "2024-08-16 23:58:36.965000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "5", + "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", + "volume_id": null + }, + { + "id": "fe628193-48d4-463e-b850-875818fe596f", + "created_date": "2024-08-16 23:58:36.574000", + "last_modified_date": "2024-08-16 23:58:36.574000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "0095a769-e5e2-441e-8a38-f98743ee9529", + "volume_id": null + }, + { + "id": "fed14271-c3dc-4b46-999e-16bf238ddbbb", + "created_date": "2024-08-16 23:58:35.736000", + "last_modified_date": "2024-08-16 23:58:35.736000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "21", + "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", + "volume_id": null + }, + { + "id": "fee4ab2b-ae91-4df1-996e-5603321ef571", + "created_date": "2024-08-16 23:58:36.894000", + "last_modified_date": "2024-08-16 23:58:36.894000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "0fd9969e-8e1f-4f42-94ac-fab4d2d614c2", + "volume_id": null + }, + { + "id": "ff6136be-6e6d-4f71-9621-b5810fba6790", + "created_date": "2024-08-16 23:58:36.747000", + "last_modified_date": "2024-08-16 23:58:36.747000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "3", + "comic_id": "fffaa1a6-5c3d-4deb-b759-35de74c65958", + "volume_id": null + }, + { + "id": "ff869dfc-edae-4cc9-bbf7-ec53e79f0a19", + "created_date": "2024-08-16 23:58:37.089000", + "last_modified_date": "2024-08-16 23:58:37.089000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "14a359a2-c928-4e14-9fb2-249777e6a13a", + "volume_id": null + }, + { + "id": "ff9c913d-7c2d-4805-aef4-8b0184c9ebe7", + "created_date": "2024-08-16 23:58:36.580000", + "last_modified_date": "2024-08-16 23:58:36.580000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "1", + "comic_id": "8955b2a3-84d3-4b55-a1f1-d45193600861", + "volume_id": null + }, + { + "id": "ffa94296-9736-4d2d-8447-202e137b970b", + "created_date": "2024-08-16 23:58:36.334000", + "last_modified_date": "2024-08-16 23:58:36.334000", + "version": 0, + "in_stock": 0, + "is_read": 0, + "issue_number": "2", + "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", + "volume_id": null + } + ], + "book_author": [], + "trade_paperback": [ + { + "id": "02386448-b7b0-4ae2-92c9-1db4fb1ef51b", + "created_date": "2024-08-16 23:58:35.576000", + "last_modified_date": "2024-08-16 23:58:35.576000", + "version": 0, + "issue_start": 40, + "issue_end": 45, + "name": "Until the Stars Turn Cold", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261" + }, + { + "id": "05e79848-fdcb-4914-82bd-26e56adfd630", + "created_date": "2024-08-16 23:58:35.533000", + "last_modified_date": "2024-08-16 23:58:35.533000", + "version": 0, + "issue_start": 8, + "issue_end": 14, + "name": "Blood For Blood", + "comic_id": "8ad36a79-9436-455d-8096-0f1b73c22f13" + }, + { + "id": "1a58e34c-7048-4882-bec3-57a93dda68f6", + "created_date": "2024-08-16 23:58:35.574000", + "last_modified_date": "2024-08-16 23:58:35.574000", + "version": 0, + "issue_start": 36, + "issue_end": 39, + "name": "Revelations", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261" + }, + { + "id": "1e9d193a-743c-43df-a202-0e1e159528ee", + "created_date": "2024-08-16 23:58:35.490000", + "last_modified_date": "2024-08-16 23:58:35.490000", + "version": 0, + "issue_start": 1, + "issue_end": 18, + "name": "Vol. 1", + "comic_id": "134659ae-67a4-4cad-aac6-36bee893102f" + }, + { + "id": "203ad009-5467-4df3-b543-14f167d43556", + "created_date": "2024-08-16 23:58:35.581000", + "last_modified_date": "2024-08-16 23:58:35.581000", + "version": 0, + "issue_start": 51, + "issue_end": 56, + "name": "Unintended Consequences", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261" + }, + { + "id": "247b0218-7554-4d9c-be9d-7cdc44c34670", + "created_date": "2024-08-16 23:58:35.508000", + "last_modified_date": "2024-08-16 23:58:35.508000", + "version": 0, + "issue_start": 15, + "issue_end": 20, + "name": "Taking The Skies", + "comic_id": "2a4b287e-4b05-4016-8eb4-21fc225be24d" + }, + { + "id": "24a0194c-bc81-4915-9270-8df6d0362bd8", + "created_date": "2024-08-16 23:58:35.492000", + "last_modified_date": "2024-08-16 23:58:35.492000", + "version": 0, + "issue_start": 1, + "issue_end": 5, + "name": "Choices", + "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27" + }, + { + "id": "264b7882-653e-4dc1-821a-2803e8e6c742", + "created_date": "2024-08-16 23:58:35.503000", + "last_modified_date": "2024-08-16 23:58:35.503000", + "version": 0, + "issue_start": 1, + "issue_end": 7, + "name": "Flying Solo", + "comic_id": "2a4b287e-4b05-4016-8eb4-21fc225be24d" + }, + { + "id": "294c0f2c-5a2b-46d3-939f-2dc6c2f8a82c", + "created_date": "2024-08-16 23:58:35.583000", + "last_modified_date": "2024-08-16 23:58:35.583000", + "version": 0, + "issue_start": 500, + "issue_end": 502, + "name": "Happy Birthday", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261" + }, + { + "id": "29cfdd9a-9d7b-4c22-a291-c6dc3d71fd9a", + "created_date": "2024-08-16 23:58:35.558000", + "last_modified_date": "2024-08-16 23:58:35.558000", + "version": 0, + "issue_start": 13, + "issue_end": 18, + "name": "Earth Angel", + "comic_id": "56152b4f-9a84-40ea-a329-8a267d931182" + }, + { + "id": "2fc730b6-bd70-4f5c-8d33-e43c97115a39", + "created_date": "2024-08-16 23:58:35.510000", + "last_modified_date": "2024-08-16 23:58:35.510000", + "version": 0, + "issue_start": 21, + "issue_end": 26, + "name": "Coming Home", + "comic_id": "2a4b287e-4b05-4016-8eb4-21fc225be24d" + }, + { + "id": "327e1360-b942-4559-b187-90f9032d28f4", + "created_date": "2024-08-16 23:58:35.527000", + "last_modified_date": "2024-08-16 23:58:35.527000", + "version": 0, + "issue_start": 7, + "issue_end": 12, + "name": "Superhuman Law", + "comic_id": "e54ebebe-701a-4d60-88ce-4df9b34da6ca" + }, + { + "id": "362a4300-e0fb-4092-8f74-d1605c1541a8", + "created_date": "2024-08-16 23:58:35.483000", + "last_modified_date": "2024-08-16 23:58:35.483000", + "version": 0, + "issue_start": 13, + "issue_end": 18, + "name": "The Warriors Tale", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc" + }, + { + "id": "38feab7c-dc44-4a25-8cad-c72727551e84", + "created_date": "2024-08-16 23:58:35.530000", + "last_modified_date": "2024-08-16 23:58:35.530000", + "version": 0, + "issue_start": 1, + "issue_end": 7, + "name": "Conflict of Conscience", + "comic_id": "8ad36a79-9436-455d-8096-0f1b73c22f13" + }, + { + "id": "3a524877-b2fc-4d3c-b23e-df0b565e9322", + "created_date": "2024-08-16 23:58:35.546000", + "last_modified_date": "2024-08-16 23:58:35.546000", + "version": 0, + "issue_start": 1, + "issue_end": 6, + "name": "Public Enemies", + "comic_id": "ed58b16e-0701-4373-befe-39118bc2d4cb" + }, + { + "id": "58dd3153-ce56-4b33-92d9-3e7b2d27f79f", + "created_date": "2024-08-16 23:58:35.500000", + "last_modified_date": "2024-08-16 23:58:35.500000", + "version": 0, + "issue_start": 13, + "issue_end": 18, + "name": "Strangers in Atlantis", + "comic_id": "2ca8751f-8d0a-449e-933f-f293e6fbd751" + }, + { + "id": "5ae999a1-9863-4caa-9c7a-f5bf3338344d", + "created_date": "2024-08-16 23:58:35.554000", + "last_modified_date": "2024-08-16 23:58:35.554000", + "version": 0, + "issue_start": 7, + "issue_end": 12, + "name": "Heaven & Earth", + "comic_id": "56152b4f-9a84-40ea-a329-8a267d931182" + }, + { + "id": "62a262e5-a284-472c-8617-cb314164c61e", + "created_date": "2024-08-16 23:58:35.566000", + "last_modified_date": "2024-08-16 23:58:35.566000", + "version": 0, + "issue_start": 1, + "issue_end": 8, + "name": "1602", + "comic_id": "94837af3-bbaf-4496-b114-1676a3271cb6" + }, + { + "id": "74e93758-8c2a-4e5d-9d1c-f69835a439ea", + "created_date": "2024-08-16 23:58:35.524000", + "last_modified_date": "2024-08-16 23:58:35.524000", + "version": 0, + "issue_start": 1, + "issue_end": 6, + "name": "Single Green Female", + "comic_id": "e54ebebe-701a-4d60-88ce-4df9b34da6ca" + }, + { + "id": "8d1293f9-3f8b-4cc1-8a66-25799dbe2208", + "created_date": "2024-08-16 23:58:35.520000", + "last_modified_date": "2024-08-16 23:58:35.520000", + "version": 0, + "issue_start": 21, + "issue_end": 26, + "name": "Out All Night", + "comic_id": "1b7de491-6bbb-404e-a2e5-a20a123e3fe5" + }, + { + "id": "8e0228ef-8d67-4475-befb-04745ef571ac", + "created_date": "2024-08-16 23:58:35.486000", + "last_modified_date": "2024-08-16 23:58:35.486000", + "version": 0, + "issue_start": 19, + "issue_end": 24, + "name": "The Thiefs Tale", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc" + }, + { + "id": "9a6436f7-53ad-43d3-871f-e727cb757391", + "created_date": "2024-08-16 23:58:35.562000", + "last_modified_date": "2024-08-16 23:58:35.562000", + "version": 0, + "issue_start": 19, + "issue_end": 24, + "name": "Redemption", + "comic_id": "56152b4f-9a84-40ea-a329-8a267d931182" + }, + { + "id": "9b9bac86-8be9-4d9d-82a7-1fe69dc101a9", + "created_date": "2024-08-16 23:58:35.479000", + "last_modified_date": "2024-08-16 23:58:35.479000", + "version": 0, + "issue_start": 7, + "issue_end": 12, + "name": "The Dragons Tale", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc" + }, + { + "id": "a5993845-f7e2-4996-a9b7-99e79f972d61", + "created_date": "2024-08-16 23:58:35.505000", + "last_modified_date": "2024-08-16 23:58:35.505000", + "version": 0, + "issue_start": 8, + "issue_end": 14, + "name": "Going To Ground", + "comic_id": "2a4b287e-4b05-4016-8eb4-21fc225be24d" + }, + { + "id": "b957bf23-07df-439c-a4f4-7529601438c0", + "created_date": "2024-08-16 23:58:35.512000", + "last_modified_date": "2024-08-16 23:58:35.512000", + "version": 0, + "issue_start": 1, + "issue_end": 7, + "name": "Rite of Passage", + "comic_id": "1b7de491-6bbb-404e-a2e5-a20a123e3fe5" + }, + { + "id": "bda23863-2c95-4fd4-8486-2f514b70920e", + "created_date": "2024-08-16 23:58:35.472000", + "last_modified_date": "2024-08-16 23:58:35.472000", + "version": 0, + "issue_start": 1, + "issue_end": 12, + "name": "Vol. 1", + "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369" + }, + { + "id": "beb2a1eb-1c40-45f7-a0ff-6f515821beca", + "created_date": "2024-08-16 23:58:35.587000", + "last_modified_date": "2024-08-16 23:58:35.587000", + "version": 0, + "issue_start": 56, + "issue_end": 61, + "name": "Of Like Minds", + "comic_id": "4a5c0ca1-9a08-49b9-bf7c-7bde4f35551a" + }, + { + "id": "c11cad0a-8501-4072-af1c-66ad28a08a71", + "created_date": "2024-08-16 23:58:35.578000", + "last_modified_date": "2024-08-16 23:58:35.578000", + "version": 0, + "issue_start": 46, + "issue_end": 50, + "name": "The Life & Death of Spiders", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261" + }, + { + "id": "ca73f9cf-112b-46e6-907f-adba5e9f9327", + "created_date": "2024-08-16 23:58:35.476000", + "last_modified_date": "2024-08-16 23:58:35.476000", + "version": 0, + "issue_start": 1, + "issue_end": 6, + "name": "From The Ashes", + "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc" + }, + { + "id": "cb129693-5418-4e2f-b2d6-f59445155f02", + "created_date": "2024-08-16 23:58:35.497000", + "last_modified_date": "2024-08-16 23:58:35.497000", + "version": 0, + "issue_start": 7, + "issue_end": 12, + "name": "Test Of Time", + "comic_id": "2ca8751f-8d0a-449e-933f-f293e6fbd751" + }, + { + "id": "d092c5be-a1ed-42bc-8ccc-110515af2122", + "created_date": "2024-08-16 23:58:35.550000", + "last_modified_date": "2024-08-16 23:58:35.550000", + "version": 0, + "issue_start": 1, + "issue_end": 6, + "name": "Loyalty And Loss", + "comic_id": "56152b4f-9a84-40ea-a329-8a267d931182" + }, + { + "id": "d1f8a566-4d12-4f2e-b1be-6024f54e517e", + "created_date": "2024-08-16 23:58:35.495000", + "last_modified_date": "2024-08-16 23:58:35.495000", + "version": 0, + "issue_start": 1, + "issue_end": 6, + "name": "Atlantis Rising", + "comic_id": "2ca8751f-8d0a-449e-933f-f293e6fbd751" + }, + { + "id": "d43e483a-60ca-48b9-a6ce-ab4b171b2edb", + "created_date": "2024-08-16 23:58:35.539000", + "last_modified_date": "2024-08-16 23:58:35.539000", + "version": 0, + "issue_start": 22, + "issue_end": 27, + "name": "Sanctuary", + "comic_id": "8ad36a79-9436-455d-8096-0f1b73c22f13" + }, + { + "id": "e118cba1-0f8f-4e10-99dd-de4e28c07bb3", + "created_date": "2024-08-16 23:58:35.517000", + "last_modified_date": "2024-08-16 23:58:35.517000", + "version": 0, + "issue_start": 15, + "issue_end": 20, + "name": "Siege of Scales", + "comic_id": "1b7de491-6bbb-404e-a2e5-a20a123e3fe5" + }, + { + "id": "e5a97bc1-9d4b-4d7a-b730-6307d1894832", + "created_date": "2024-08-16 23:58:35.585000", + "last_modified_date": "2024-08-16 23:58:35.585000", + "version": 0, + "issue_start": 1, + "issue_end": 2, + "name": "Sonderband 1", + "comic_id": "03c5b145-69d4-4d7e-8323-cb2f81060829" + }, + { + "id": "e847f422-78a8-42cc-a1a4-cfcff1d86729", + "created_date": "2024-08-16 23:58:35.569000", + "last_modified_date": "2024-08-16 23:58:35.569000", + "version": 0, + "issue_start": 30, + "issue_end": 35, + "name": "Coming Home", + "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261" + }, + { + "id": "f0de5b20-d172-41e1-a079-7cdf19da9f6e", + "created_date": "2024-08-16 23:58:35.590000", + "last_modified_date": "2024-08-16 23:58:35.590000", + "version": 0, + "issue_start": 62, + "issue_end": 68, + "name": "Sensei & Student", + "comic_id": "4a5c0ca1-9a08-49b9-bf7c-7bde4f35551a" + }, + { + "id": "f9daa8a2-4a33-4c38-91da-2d79f15eeac3", + "created_date": "2024-08-16 23:58:35.536000", + "last_modified_date": "2024-08-16 23:58:35.536000", + "version": 0, + "issue_start": 15, + "issue_end": 21, + "name": "Divided Loyalties", + "comic_id": "8ad36a79-9436-455d-8096-0f1b73c22f13" + }, + { + "id": "fba9cccb-2217-465c-97c7-874ae74b2cba", + "created_date": "2024-08-16 23:58:35.542000", + "last_modified_date": "2024-08-16 23:58:35.542000", + "version": 0, + "issue_start": 444, + "issue_end": 449, + "name": "The End Of History", + "comic_id": "b6383b7f-1c86-42d9-a3f6-d2a4bc96dc51" + }, + { + "id": "ffac4cc3-8111-4579-b689-a768603179a1", + "created_date": "2024-08-16 23:58:35.514000", + "last_modified_date": "2024-08-16 23:58:35.514000", + "version": 0, + "issue_start": 8, + "issue_end": 14, + "name": "The Demon Queen", + "comic_id": "1b7de491-6bbb-404e-a2e5-a20a123e3fe5" + } + ], + "mail": [] +} \ No newline at end of file diff --git a/python/kontor-gui/gui/comic_window.py b/python/kontor-gui/gui/comic_window.py new file mode 100644 index 0000000..5d9b747 --- /dev/null +++ b/python/kontor-gui/gui/comic_window.py @@ -0,0 +1,54 @@ +from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QTabWidget, QMenu, QTableView + +from gui.model_config import KontorModelConfig +from gui.table_model import KontorTableModel + + +class ComicWindow(QWidget): + def __init__(self, main_window): + super().__init__() + self.statusBar = main_window.statusBar + self._main_window = main_window + self.data_views = list() + # self._create_menubar() + self._init_gui() + + def _init_gui(self): + self.central_widget = QWidget() + layout = QVBoxLayout() + #self.central_widget.setLayout(parent_layout) + self.tabs = QTabWidget() + self.tabs.addTab(self.generate_data_tab("comic"), "Comics") + self.tabs.addTab(self.generate_data_tab("publisher"), "Publisher") + self.tabs.currentChanged.connect(self._tab_changed) + # label.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(self.tabs) + self.setLayout(layout) + # self.setCentralWidget(self.central_widget) + + # def _create_menubar(self): + # menu_bar = self.menuBar() + # comic_menu = QMenu("Comic") + # menu_bar.addMenu(comic_menu) + + def refresh(self): + self.data_views[self.tabs.currentIndex()].refresh() + + def _tab_changed(self, tab_index): + self.data_views[tab_index].refresh() + + def generate_data_tab(self, table_name): + data_tab = QWidget() + + table_config = KontorModelConfig(self._main_window.kontor_db, self, table_name) + model = KontorTableModel(table_config) + layout = QVBoxLayout() + self.data_views.append(model) + data_tab.setLayout(layout) + table_view = QTableView() + table_view.setModel(model) + layout.addLayout(table_config.get_filter_layout()) + layout.addWidget(table_view) + model.refresh() + return data_tab + diff --git a/python/kontor-gui/gui/main_window.py b/python/kontor-gui/gui/main_window.py index 739212e..751795d 100644 --- a/python/kontor-gui/gui/main_window.py +++ b/python/kontor-gui/gui/main_window.py @@ -4,6 +4,7 @@ from PySide6.QtWidgets import QLabel, QMainWindow from sqlalchemy import Engine from kontor_schema import KontorDB +from .comic_window import ComicWindow from .progress import ProgressUpdate from .dialogs import ExportKontorDialog, ImportKontorDialog from .model_config import KontorModelConfig @@ -34,6 +35,8 @@ class MainWindow(QMainWindow): self.filter = {} self.kontor_db = KontorDB(engine, log) self.log = log + + self.comic_window = ComicWindow(self) self.central_widget = QWidget() parent_layout = QVBoxLayout() self.central_widget.setLayout(parent_layout) @@ -52,6 +55,8 @@ class MainWindow(QMainWindow): self.newAction = QAction("&New", self) self.aboutAction = QAction("&Über...", self) self.aboutAction.triggered.connect(self.about) + self.showComicWindow = QAction("Show/Hide &Comic Window", self) + self.showComicWindow.triggered.connect(self.show_comic_window) self.importAction = QAction(self.import_icon, "&Import", self) self.importAction.triggered.connect(self.import_from_file) self.exportAction = QAction(self.export_icon, "&Export", self) @@ -80,6 +85,7 @@ class MainWindow(QMainWindow): kontor_menu.addAction(self.importAction) kontor_menu.addAction(self.exportAction) comic_menu = QMenu("&Comic") + comic_menu.addAction(self.showComicWindow) tysc_menu = QMenu("&TradeYourSportCards") media_file_menu = QMenu("&MediaFile") media_file_menu.addAction(self.updateTitleAction) @@ -110,12 +116,18 @@ class MainWindow(QMainWindow): def about(self): QMessageBox.about(self.central_widget, "Über Kontor", f"Python: 3.11\nKontor: 0.1.0") + def show_comic_window(self): + if self.comic_window.isHidden(): + self.comic_window.show() + else: + self.comic_window.hide() + def import_from_file(self): import_dlg = ImportKontorDialog(self) if import_dlg.exec(): print(f"import DB from file {import_dlg.file_name}") else: - print("no nothing for import") + print("do nothing for import") pass def export_to_file(self): diff --git a/python/kontor-gui/kontor.py b/python/kontor-gui/kontor.py index 50e16cd..57e7251 100644 --- a/python/kontor-gui/kontor.py +++ b/python/kontor-gui/kontor.py @@ -1,7 +1,6 @@ """ -PyQT6 GUI for Kontor +PySide6 GUI for Kontor """ -import logging import sys import logging.config from pathlib import Path diff --git a/python/kontor-schema/kontor_schema/__init__.py b/python/kontor-schema/kontor_schema/__init__.py index a26e041..be7eea2 100644 --- a/python/kontor-schema/kontor_schema/__init__.py +++ b/python/kontor-schema/kontor_schema/__init__.py @@ -3,12 +3,15 @@ import re import subprocess import uuid from datetime import datetime +from logging import Logger from pathlib import Path -from sqlalchemy import Engine +import mariadb +from sqlalchemy import Engine, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import sessionmaker +from .base import Base, BaseMixin from .admin import User, Token, Role, AuthorizationMatrix, ModuleData, MailAccount, Mail from .bookshelf import Article, Book, Author, BookshelfPublisher, ArticleAuthor, BookAuthor from .comic import Comic, Artist, Publisher, Issue, StoryArc, TradePaperback, Volume, ComicWork, WorkType @@ -19,44 +22,45 @@ from .media import MediaFile, MediaArticle, MediaVideo class KontorDB: - def __init__(self, db_engine: Engine): + def __init__(self, db_engine: Engine, log: Logger): self.engine = db_engine self.registry = {} self.init_registry() + self.log = log def init_registry(self): - self.registry['card'] = Card - self.registry['card_set'] = CardSet - self.registry['sport'] = Sport - self.registry['team'] = Team - self.registry['field_position'] = FieldPosition - self.registry['rooster'] = Rooster - self.registry['player'] = Player - self.registry['vendor'] = Vendor - self.registry['artist'] = Artist - self.registry['publisher'] = Publisher - self.registry['comic'] = Comic - self.registry['issue'] = Issue - self.registry['story_arc'] = StoryArc - self.registry['trade_paperback'] = TradePaperback - self.registry['volume'] = Volume - self.registry['comic_work'] = ComicWork - self.registry['worktype'] = WorkType - self.registry['article'] = Article - self.registry['book'] = Book - self.registry['author'] = Author - self.registry['bookshelf_publisher'] = BookshelfPublisher - self.registry['article_author'] = ArticleAuthor - self.registry['book_author'] = BookAuthor - self.registry['media_file'] = MediaFile - self.registry['media_article'] = MediaArticle - self.registry['media_video'] = MediaVideo - self.registry[MetaDataTable.__tablename__] = MetaDataTable + self.registry[Card.__tablename__] = Card + self.registry[CardSet.__tablename__] = CardSet + self.registry[Rooster.__tablename__] = Rooster + self.registry[Team.__tablename__] = Team + self.registry[FieldPosition.__tablename__] = FieldPosition + self.registry[Player.__tablename__] = Player + self.registry[Vendor.__tablename__] = Vendor + self.registry[Sport.__tablename__] = Sport + self.registry[Issue.__tablename__] = Issue + self.registry[TradePaperback.__tablename__] = TradePaperback + self.registry[StoryArc.__tablename__] = StoryArc + self.registry[Volume.__tablename__] = Volume + self.registry[ComicWork.__tablename__] = ComicWork + self.registry[Artist.__tablename__] = Artist + self.registry[Comic.__tablename__] = Comic + self.registry[Publisher.__tablename__] = Publisher + self.registry[WorkType.__tablename__] = WorkType + self.registry[ArticleAuthor.__tablename__] = ArticleAuthor + self.registry[BookAuthor.__tablename__] = BookAuthor + self.registry[BookshelfPublisher.__tablename__] = BookshelfPublisher + self.registry[Article.__tablename__] = Article + self.registry[Book.__tablename__] = Book + self.registry[Author.__tablename__] = Author + self.registry[MediaFile.__tablename__] = MediaFile + self.registry[MediaArticle.__tablename__] = MediaArticle + self.registry[MediaVideo.__tablename__] = MediaVideo self.registry[MetaDataColumn.__tablename__] = MetaDataColumn - self.registry[User.__tablename__] = User - self.registry[Token.__tablename__] = Token - self.registry[Role.__tablename__] = Role + self.registry[MetaDataTable.__tablename__] = MetaDataTable self.registry[AuthorizationMatrix.__tablename__] = AuthorizationMatrix + self.registry[Token.__tablename__] = Token + self.registry[User.__tablename__] = User + self.registry[Role.__tablename__] = Role self.registry[ModuleData.__tablename__] = ModuleData self.registry[MailAccount.__tablename__] = MailAccount self.registry[Mail.__tablename__] = Mail @@ -65,7 +69,7 @@ class KontorDB: result = [] __session__ = sessionmaker(self.engine) with __session__() as session: - tables = session.query(MetaDataTable).all() + tables = session.scalars(select(MetaDataTable)).all() result = [table.table_name for table in tables] return result @@ -123,9 +127,9 @@ class KontorDB: with __session__() as session: entries = [] if len(filters) == 0: - entries = session.query(table).all() + entries = session.scalars(select(table)).all() else: - entries = session.query(table).filter_by(**filters) + entries = session.scalars(select(table).filter_by(**filters)).all() for entry in entries: row = [] for order in columns.keys(): @@ -149,7 +153,7 @@ class KontorDB: if table in self.registry: model = self.registry[table] else: - print(f"table {table} is not registered") + self.log.info(f"table {table} is not registered") continue __session__ = sessionmaker(self.engine) with __session__() as session: @@ -182,13 +186,16 @@ class KontorDB: export_file = Path(export_file_name) case "SQLite": export_file = Path(export_file_name) + self.log.info("%d tables exported", len(results)) return results - def import_db(self, import_file_name: str, dry_run: bool) -> dict: + def import_db(self, import_file_name: str, delete_first: bool) -> dict: result = {} + if delete_first: + self.delete_entries() import_file = Path(import_file_name) if not import_file.exists(): - print(f"File {import_file_name} does not exist. Do nothing.") + self.log.info("File %s does not exist. Do nothing.", import_file_name) return result match import_file.suffix: case '.json': @@ -196,8 +203,8 @@ class KontorDB: with open(import_file_name, 'r') as json_file: json_load = json.load(json_file) for table in json_load: - print(f"{table}: {len(json_load[table])}") - result[table] = self.import_table(table, json_load[table], dry_run) + self.log.info("%s: %d", table, len(json_load[table])) + result[table] = self.import_table(table, json_load[table]) case '.yml': print("read yaml file") case '.yaml': @@ -206,30 +213,36 @@ class KontorDB: print("read sqlite file") return result - def import_table(self, table_name, items, dry_run: bool) -> dict: + def import_table(self, table_name: str, items:list) -> dict: result = {} updated = [] added = [] remaining = [] existing_ids = self.get_ids(table_name) + self.log.info("found %d existing ids for table %s", len(existing_ids), table_name) for item in items: current_id = item['id'] + # print(f"import item: {item}") found_item = None __session__ = sessionmaker(self.engine) with __session__() as session: - found_item = session.query(self.registry[table_name]).get(current_id) + found_item = session.get(self.registry[table_name], current_id) + # print(f"found item: {found_item}") if found_item is not None: changed = self.update_entry(table_name, current_id, item) updated.append(item) if changed: - print(f"{current_id} has changed") + self.log.info("%s has changed", current_id) updated.append(item) existing_ids.remove(current_id) else: - self.add_entry(table_name, item) - added.append(item) + try: + self.add_entry(table_name, item) + added.append(item) + except IntegrityError as error: + self.log.info("Could not add item, due to: %s", error.detail) if len(existing_ids) > 0: - print("remaining items") + print(f"remaining items: {existing_ids}") remaining.extend(existing_ids) result['updated'] = updated result['added'] = added @@ -246,16 +259,18 @@ class KontorDB: return existing_ids def add_entry(self, table_name: str, update_item: dict): + self.log.info("add entry to table %s with %s", table_name, update_item) __session__ = sessionmaker(self.engine) with __session__() as session: add_item = self.registry[table_name]() for key in update_item.keys(): update_value = update_item[key] setattr(add_item, key, update_value) - session.add(add_item) - session.commit() + session.add(add_item) + session.commit() def update_entry(self, table_name, current_id, update_item: dict) -> bool: + self.log.info("update entry to table %s", table_name) __session__ = sessionmaker(self.engine) with __session__() as session: existing_item = session.query(self.registry[table_name]).get(current_id) @@ -266,11 +281,11 @@ class KontorDB: if type(existing_value) is not type(update_value): existing_value = str(existing_value) if existing_value != update_value: - print(f"{key} has changed: {existing_value} != {update_value}") + self.log.info("%s has changed: %s != %s", key, existing_value, update_value) setattr(existing_item, key, update_value) session.commit() changed = True - print(f"update {key} with {update_value}") + self.log.info("update %s with %s", key, update_value) return changed def add_link(self, link: str) -> dict: @@ -317,3 +332,13 @@ class KontorDB: continue download_list[link.id] = url return download_list + + def delete_entries(self): + for (table_name, table) in self.registry.items(): + self.log.info("delete entries from table %s", table_name) + __session__ = sessionmaker(self.engine) + with __session__() as session: + items = session.query(table).all() + for item in items: + session.delete(item) + session.commit() diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java index 3f12472..5350f69 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java @@ -166,11 +166,11 @@ public class SetupModuleAdmin implements ApplicationListener Date: Tue, 21 Jan 2025 20:12:50 +0100 Subject: [PATCH 61/91] Refactor GUI to use Multi Document Interface --- python/kontor-gui/gui/comic_window.py | 33 +++++--- python/kontor-gui/gui/main_window.py | 83 +++++++++++++------ python/kontor-gui/gui/model_config.py | 20 +++-- python/kontor-gui/gui/table_model.py | 42 +++++----- .../kontor-schema/kontor_schema/__init__.py | 12 ++- 5 files changed, 122 insertions(+), 68 deletions(-) diff --git a/python/kontor-gui/gui/comic_window.py b/python/kontor-gui/gui/comic_window.py index 5d9b747..b9f9b9f 100644 --- a/python/kontor-gui/gui/comic_window.py +++ b/python/kontor-gui/gui/comic_window.py @@ -1,42 +1,49 @@ -from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QTabWidget, QMenu, QTableView +from PySide6.QtCore import Signal +from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QTabWidget, QMenu, QTableView, QMdiSubWindow from gui.model_config import KontorModelConfig from gui.table_model import KontorTableModel -class ComicWindow(QWidget): +class ComicWindow(QMdiSubWindow): + closed = Signal() + def __init__(self, main_window): super().__init__() - self.statusBar = main_window.statusBar - self._main_window = main_window self.data_views = list() - # self._create_menubar() + self._main_window = main_window + self.log = main_window.log self._init_gui() + self.tick = main_window.tick + self.cross = main_window.cross def _init_gui(self): - self.central_widget = QWidget() + self.setWindowTitle("Comics") + self.setWidget(QWidget()) layout = QVBoxLayout() - #self.central_widget.setLayout(parent_layout) self.tabs = QTabWidget() self.tabs.addTab(self.generate_data_tab("comic"), "Comics") self.tabs.addTab(self.generate_data_tab("publisher"), "Publisher") self.tabs.currentChanged.connect(self._tab_changed) - # label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(self.tabs) self.setLayout(layout) - # self.setCentralWidget(self.central_widget) + self.setWidget(self.tabs) - # def _create_menubar(self): - # menu_bar = self.menuBar() - # comic_menu = QMenu("Comic") - # menu_bar.addMenu(comic_menu) + def closeEvent(self, event): + self.closed.emit() + super().closeEvent(event) + self._main_window.remove_sub_window('comic') def refresh(self): + # self.log.info("refresh") self.data_views[self.tabs.currentIndex()].refresh() def _tab_changed(self, tab_index): self.data_views[tab_index].refresh() + def update_status(self, message): + self._main_window.update_status(message) + def generate_data_tab(self, table_name): data_tab = QWidget() diff --git a/python/kontor-gui/gui/main_window.py b/python/kontor-gui/gui/main_window.py index 751795d..384723b 100644 --- a/python/kontor-gui/gui/main_window.py +++ b/python/kontor-gui/gui/main_window.py @@ -1,5 +1,5 @@ from PySide6.QtGui import QAction, QIcon, QGuiApplication -from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView, QProgressBar +from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView, QProgressBar, QMdiArea from PySide6.QtWidgets import QLabel, QMainWindow from sqlalchemy import Engine from kontor_schema import KontorDB @@ -22,32 +22,39 @@ class MainWindow(QMainWindow): self.export_icon = QIcon("res/application-export.png") self.circle_icon = QIcon("res/arrow-circle-double.png") + self.data = [] + self.filter = {} + self.kontor_db = KontorDB(engine, log) + self.log = log + self._subwindows = {} + + self._setup_ui() + + + #self.tabs = QTabWidget() + #self.tabs.addTab(self.generate_data_tab("comic"), "Comics") + #self.tabs.addTab(self.generate_data_tab("media_file"), "MediaFile") + #self.tabs.currentChanged.connect(self._tab_changed) + #label.setAlignment(Qt.AlignmentFlag.AlignCenter) + #parent_layout.addWidget(self.tabs) + + self.setCentralWidget(self.central_widget) + + def _setup_ui(self): self.setWindowTitle("Kontor") - self.setMinimumSize(800, 500) + self.setMinimumSize(1200, 800) self._create_actions() + self.central_widget = QWidget() + # parent_layout = QVBoxLayout() + # self.central_widget.setLayout(parent_layout) + self.mdi_area = QMdiArea(self.central_widget) + self.mdi_area.setObjectName('mdi_area') + self.setCentralWidget(self.central_widget) self._create_menubar() self._create_toolbars() self.status_progress = QProgressBar() self.progress_update = ProgressUpdate(self.status_progress) self._create_statusbar() - - self.data = [] - self.filter = {} - self.kontor_db = KontorDB(engine, log) - self.log = log - - self.comic_window = ComicWindow(self) - self.central_widget = QWidget() - parent_layout = QVBoxLayout() - self.central_widget.setLayout(parent_layout) - self.tabs = QTabWidget() - self.tabs.addTab(self.generate_data_tab("comic"), "Comics") - self.tabs.addTab(self.generate_data_tab("media_file"), "MediaFile") - self.tabs.currentChanged.connect(self._tab_changed) - #label.setAlignment(Qt.AlignmentFlag.AlignCenter) - parent_layout.addWidget(self.tabs) - - self.setCentralWidget(self.central_widget) centerPoint = QGuiApplication.screens()[0].geometry().center() self.move(centerPoint - self.frameGeometry().center()) @@ -117,10 +124,26 @@ class MainWindow(QMainWindow): QMessageBox.about(self.central_widget, "Über Kontor", f"Python: 3.11\nKontor: 0.1.0") def show_comic_window(self): - if self.comic_window.isHidden(): - self.comic_window.show() + if 'comic' not in self._subwindows: + comic = ComicWindow(self) + comic.closed.connect(self.sub_window_closed) + self._subwindows['comic'] = comic + self.mdi_area.addSubWindow(comic) + comic.show() else: - self.comic_window.hide() + comic = self._subwindows.pop('comic') + comic.close() + self.mdi_area.removeSubWindow(comic) + + def remove_sub_window(self, name: str): + # self.log.info("remove subwindow %s", name) + if name in self._subwindows: + window = self._subwindows.pop(name) + window.close() + self.mdi_area.removeSubWindow(window) + + def sub_window_closed(self): + self.log.info("close subwindow") def import_from_file(self): import_dlg = ImportKontorDialog(self) @@ -162,10 +185,17 @@ class MainWindow(QMainWindow): self.kontor_db.check_files() def refresh(self): - self.data[self.tabs.currentIndex()].refresh() + self.log.info("refresh") + for (_, window) in self._subwindows.items(): + window.refresh() + # def refresh(self): + # self.data[self.tabs.currentIndex()].refresh() - def _tab_changed(self, tab_index): - self.data[tab_index].refresh() + # def _tab_changed(self, tab_index): + # self.data[tab_index].refresh() + + def update_status(self, message, timeout=3000): + self.statusBar.showMessage(message, timeout=timeout) def generate_data_tab(self, table_name): data_tab = QWidget() @@ -181,3 +211,4 @@ class MainWindow(QMainWindow): layout.addWidget(table_view) model.refresh() return data_tab + diff --git a/python/kontor-gui/gui/model_config.py b/python/kontor-gui/gui/model_config.py index e791752..7aa05cc 100644 --- a/python/kontor-gui/gui/model_config.py +++ b/python/kontor-gui/gui/model_config.py @@ -1,4 +1,4 @@ -from PySide6.QtWidgets import QHBoxLayout, QCheckBox +from PySide6.QtWidgets import QHBoxLayout, QCheckBox, QMdiSubWindow from kontor_schema import KontorDB @@ -8,15 +8,23 @@ class KontorModelConfig: self.header = {} self.filter = {} self.main_window = main_window + self.log = main_window.log self._table_name = table_name self.kontor_db = kontor_db self.get_table_config() + def __str__(self): + return f"KontorModelConfig({self._table_name})" + def get_table_config(self): + # self.log.info("get_table_config %s", self) self.header = self.kontor_db.get_column_meta_data(self._table_name) self.filter = self.kontor_db.get_filters(self._table_name) + # self.log.info("headers: %s", self.header) + # self.log.info("%s filters: %s", self, self.filter) def filters(self) -> dict: + # self.log.info("%s filters: %s", self, self.filter) _filters = {} # print(self.filter["download"].isChecked()) for column, filter_info in self.filter.items(): @@ -24,18 +32,17 @@ class KontorModelConfig: if filter_info['widget'].isChecked(): _filters[column] = True # print(f"{filter_rule=}") + # self.log.info("filters -> %s", _filters) return _filters def get_data(self) -> list: - # data = self.kontor_db.get_data(self._table_name, self.header, self.get_filter()) - # data.clear() + # self.log.info("get_data") data = self.kontor_db.data(self._table_name, self.header, self.filters()) - # print(f"KontorModelConfig.get_data: {len(data)}") - # comics = self.kontor_db.session.query(Comic).all() - # print(f'{len(comics)} Comics loaded') + # self.log.info("get_data: %d %s", len(data), data) return data def get_filter_layout(self) -> QHBoxLayout: + # self.log.info("get_filter_layout: %s", self.filter) filter_layout = QHBoxLayout() for column, filter_info in self.filter.items(): filter_checkbox = QCheckBox() @@ -44,4 +51,5 @@ class KontorModelConfig: self.filter[column]['widget'] = filter_checkbox filter_layout.addWidget(filter_checkbox) filter_layout.addStretch() + # self.log.info("get_filter_layout: %s", self.filter) return filter_layout diff --git a/python/kontor-gui/gui/table_model.py b/python/kontor-gui/gui/table_model.py index cd08ce1..6c7574b 100644 --- a/python/kontor-gui/gui/table_model.py +++ b/python/kontor-gui/gui/table_model.py @@ -13,8 +13,13 @@ class KontorTableModel(QAbstractTableModel): self._main_window = model_config.main_window self._config = model_config self._data = [] + self.log = model_config.log + + def __str__(self): + return f"KontorTableModel({self._config})" def refresh(self): + # self.log.info("refresh") data = self._config.get_data() count = 0 # print(data) @@ -27,9 +32,10 @@ class KontorTableModel(QAbstractTableModel): # print(data) # print(self._data) self.layoutChanged.emit() - self._main_window.statusBar.showMessage(f"{count} Einträge geladen", 3000) + self._main_window.update_status(f"{count} Einträge geladen") def rowCount(self, parent=QModelIndex()): + # self.log.info("rowCount %s: %d", self, len(self._data)) # The length of the outer list. if self._data is None: return 0 @@ -53,53 +59,51 @@ class KontorTableModel(QAbstractTableModel): return value if isinstance(value, bytes): if value == b'\x01': - return self._main_window.tick + return self._config.main_window.tick else: - return self._main_window.cross + return self._config.main_window.cross if isinstance(value, int): # print('{}:: {}: {}'.format(index, value, type(value))) if value == 1: - return self._main_window.tick + return self._config.main_window.tick else: - return self._main_window.cross + return self._config.main_window.cross if isinstance(value, bool): if value: - return self._main_window.tick + return self._config.main_window.tick else: - return self._main_window.cross + return self._config.main_window.cross return str(value) if role == Qt.ItemDataRole.DecorationRole: if isinstance(value, bytes): if value == b'\x01': - return self._main_window.tick + return self._config.main_window.tick else: - return self._main_window.cross + return self._config.main_window.cross if isinstance(value, int): if value == 1: - return self._main_window.tick + return self._config.main_window.tick else: - return self._main_window.cross + return self._config.main_window.cross if isinstance(value, bool): if value: - return self._main_window.tick + return self._config.main_window.tick else: - return self._main_window.cross + return self._config.main_window.cross def columnCount(self, index=QModelIndex()): - # The following takes the first sub-list, and returns - # the length (only works if all rows are an equal length) - # print(f"Header count: {len(self._config.get_header())}") + # self.log.info("rowCount %s: %d", self, len(self._config.header)) return len(self._config.header) def setData(self, index, value, role: int) -> bool: - print(index, role) + # print(index, role) if role == Qt.ItemDataRole.EditRole: self._data[index.row()][index.column()] = value - print(self._data[index.row()][index.column()]) + # print(self._data[index.row()][index.column()]) self.dataChanged.emit(index, index) return True if role == Qt.ItemDataRole.CheckStateRole: - print("role == Qt.ItemDataRole.CheckStateRole") + # print("role == Qt.ItemDataRole.CheckStateRole") checked = value == Qt.CheckState.Checked self._data[index.row()][index.column()] = checked return False diff --git a/python/kontor-schema/kontor_schema/__init__.py b/python/kontor-schema/kontor_schema/__init__.py index be7eea2..26fafd8 100644 --- a/python/kontor-schema/kontor_schema/__init__.py +++ b/python/kontor-schema/kontor_schema/__init__.py @@ -83,6 +83,7 @@ class KontorDB: filter(MetaDataTable.id == MetaDataColumn.table_id). filter(MetaDataTable.table_name == table_name). filter(MetaDataColumn.is_shown == 1).all()): + # self.log.info("get_column_meta_data: %s %s %d", column.column_name, column.column_label, column.column_order) meta_data[order] = {'column': column.column_name, 'label': column.column_label, 'order': column.column_order, 'ref_column': column.ref_column} order += 1 @@ -96,7 +97,8 @@ class KontorDB: 'ref_column': column.ref_column } order += 1 - return meta_data + # self.log.info("get_column_meta_data: %s", meta_data) + return meta_data def get_columns(self, table_name: str) -> dict: columns = {} @@ -131,6 +133,7 @@ class KontorDB: else: entries = session.scalars(select(table).filter_by(**filters)).all() for entry in entries: + # self.log.info("data: %s", entry) row = [] for order in columns.keys(): column_name = columns[order]['column'] @@ -142,6 +145,7 @@ class KontorDB: else: row.append(getattr(entry, column_name)) data.append(row) + # self.log.info("data: %s", data) return data def export_db(self, export_type: str, export_file_name: str) -> dict: @@ -259,7 +263,7 @@ class KontorDB: return existing_ids def add_entry(self, table_name: str, update_item: dict): - self.log.info("add entry to table %s with %s", table_name, update_item) + # self.log.info("add entry to table %s with %s", table_name, update_item) __session__ = sessionmaker(self.engine) with __session__() as session: add_item = self.registry[table_name]() @@ -270,7 +274,7 @@ class KontorDB: session.commit() def update_entry(self, table_name, current_id, update_item: dict) -> bool: - self.log.info("update entry to table %s", table_name) + # self.log.info("update entry to table %s", table_name) __session__ = sessionmaker(self.engine) with __session__() as session: existing_item = session.query(self.registry[table_name]).get(current_id) @@ -335,7 +339,7 @@ class KontorDB: def delete_entries(self): for (table_name, table) in self.registry.items(): - self.log.info("delete entries from table %s", table_name) + # self.log.info("delete entries from table %s", table_name) __session__ = sessionmaker(self.engine) with __session__() as session: items = session.query(table).all() From 0cc73c09aad1adc0c4079855f39cf210d42a66cb Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Tue, 21 Jan 2025 20:14:27 +0100 Subject: [PATCH 62/91] remove pyproject.toml --- python/kontor-cli/pyproject.toml | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 python/kontor-cli/pyproject.toml diff --git a/python/kontor-cli/pyproject.toml b/python/kontor-cli/pyproject.toml deleted file mode 100644 index f65e841..0000000 --- a/python/kontor-cli/pyproject.toml +++ /dev/null @@ -1,29 +0,0 @@ -[build-system] -requires = ["setuptools"] -build-backend = "setuptools.build_meta" - -[project] -name = "kontor-cli" -version = "0.1.0" -description = "Kontor CLI Application" -authors = [ - {name = "Thomas Peetz", email = "thomas.peetz@thpeetz.de"}, -] -maintainers = [ - {name = "Thomas Peetz", email = "thomas.peetz@thpeetz.de"}, -] -readme = "README.md" -classifiers = [ - "Development Status :: 3 - Alpha", - "Environment :: Console", - "Programming Language :: Python", - "Programming Language :: Python :: 3.10", - "Topic :: Utilities", -] - -dependencies = [ -] -requires-python = ">= 3.10" - -[projects.scripts] -kontor = "kontor.main:main" From 88c623edb7be2b5442a1b4b07567ecbd362b5f24 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Fri, 24 Jan 2025 05:02:08 +0100 Subject: [PATCH 63/91] subclass VideoFile --- python/kontor-cli/kontor/controllers/media.py | 16 +++++++--------- python/kontor-schema/kontor_schema/__init__.py | 4 ++-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/python/kontor-cli/kontor/controllers/media.py b/python/kontor-cli/kontor/controllers/media.py index b5ec320..da4420c 100644 --- a/python/kontor-cli/kontor/controllers/media.py +++ b/python/kontor-cli/kontor/controllers/media.py @@ -2,7 +2,7 @@ from pathlib import Path from cement import Controller, ex from kontor_schema import KontorDB -from kontor_video import VideoLink +from kontor_video import VideoLink, MediaVideo class Media(Controller): @@ -20,7 +20,7 @@ class Media(Controller): updates = db.get_update_list() self.app.log.info(f"found {len(updates)} links for update") for file_id, url in updates.items(): - link = VideoLink(url) + link = MediaVideo(url) title = link.get_title() if title is not None: db.update_entry('media_file', file_id, {'title': title, 'review': 0,}) @@ -77,14 +77,12 @@ class Media(Controller): } if self.app.pargs.link is not None: data['link_url'] = self.app.pargs.link - if self.app.pargs.dry_run: - print(f"add url {data['link_url']} to database") - else: - db = self.app.kontor_db - result = db.add_link(self.app.pargs.link) - self.log.info(result) + self.app.log.info(f"add url {data['link_url']} to database") + db = self.app.kontor_db + result = db.add_link(self.app.pargs.link) + self.app.log.info(result) else: - print("no url was given.") + self.app.log.info("no url was given.") @ex( help='check files if existing', diff --git a/python/kontor-schema/kontor_schema/__init__.py b/python/kontor-schema/kontor_schema/__init__.py index 26fafd8..bb1db87 100644 --- a/python/kontor-schema/kontor_schema/__init__.py +++ b/python/kontor-schema/kontor_schema/__init__.py @@ -285,11 +285,11 @@ class KontorDB: if type(existing_value) is not type(update_value): existing_value = str(existing_value) if existing_value != update_value: - self.log.info("%s has changed: %s != %s", key, existing_value, update_value) + self.log.info(f"{key} has changed: {existing_value} != {update_value}") setattr(existing_item, key, update_value) session.commit() changed = True - self.log.info("update %s with %s", key, update_value) + self.log.info("update {key} with {update_value}", (key, update_value)) return changed def add_link(self, link: str) -> dict: From 845f085a769b1d164252cdf65add7ccf52d4104f Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Fri, 24 Jan 2025 05:31:20 +0100 Subject: [PATCH 64/91] remove data.json --- python/kontor-gui/data.json | 34939 ---------------------------------- 1 file changed, 34939 deletions(-) delete mode 100644 python/kontor-gui/data.json diff --git a/python/kontor-gui/data.json b/python/kontor-gui/data.json deleted file mode 100644 index 5a8ef1b..0000000 --- a/python/kontor-gui/data.json +++ /dev/null @@ -1,34939 +0,0 @@ -{ - "meta_data_table": [ - { - "id": "058fa05e-2dab-4f61-8415-b5beec1fe79e", - "created_date": "2025-01-20 16:04:41.598000", - "last_modified_date": "2025-01-20 16:04:41.598000", - "version": 0, - "table_name": "meta_data_table" - }, - { - "id": "13ea1a84-f92e-4ce6-b709-fe7c1893d229", - "created_date": "2025-01-20 16:04:41.160000", - "last_modified_date": "2025-01-20 16:04:41.160000", - "version": 0, - "table_name": "volume" - }, - { - "id": "1850a718-dd07-4766-9856-591d311a4d0d", - "created_date": "2025-01-20 16:04:41.362000", - "last_modified_date": "2025-01-20 16:04:41.362000", - "version": 0, - "table_name": "sport" - }, - { - "id": "1951f2b6-3640-4568-9c47-a20e0768a843", - "created_date": "2025-01-20 16:04:41.256000", - "last_modified_date": "2025-01-20 16:04:41.256000", - "version": 0, - "table_name": "author" - }, - { - "id": "245402a4-ee6a-404b-afb5-6a95ed88469f", - "created_date": "2025-01-20 16:04:41.607000", - "last_modified_date": "2025-01-20 16:04:41.607000", - "version": 0, - "table_name": "meta_data_column" - }, - { - "id": "26478de9-0bcc-427f-ba60-0a1ab79e107b", - "created_date": "2025-01-20 16:04:41.106000", - "last_modified_date": "2025-01-20 16:04:41.106000", - "version": 0, - "table_name": "comic" - }, - { - "id": "2f841ba9-48f3-4a44-876a-67044d43b5db", - "created_date": "2025-01-20 16:04:41.009000", - "last_modified_date": "2025-01-20 16:04:41.009000", - "version": 0, - "table_name": "media_video" - }, - { - "id": "33b748ee-9f55-487a-a614-2ab64b844a13", - "created_date": "2025-01-20 16:04:41.091000", - "last_modified_date": "2025-01-20 16:04:41.091000", - "version": 0, - "table_name": "publisher" - }, - { - "id": "36c7ac9c-270d-40a4-af43-c952120f165c", - "created_date": "2025-01-20 16:04:41.536000", - "last_modified_date": "2025-01-20 16:04:41.536000", - "version": 0, - "table_name": "authorization_matrix" - }, - { - "id": "3773ac76-a957-42b2-b24d-5e66e4c32c12", - "created_date": "2025-01-20 16:04:41.409000", - "last_modified_date": "2025-01-20 16:04:41.409000", - "version": 0, - "table_name": "vendor" - }, - { - "id": "3ce78ef2-35c6-4397-8cdf-9241b52236df", - "created_date": "2025-01-20 16:04:41.438000", - "last_modified_date": "2025-01-20 16:04:41.438000", - "version": 0, - "table_name": "rooster" - }, - { - "id": "63285ce5-eb45-47f5-9575-dec1f9778bbc", - "created_date": "2025-01-20 16:04:40.957000", - "last_modified_date": "2025-01-20 16:04:40.957000", - "version": 0, - "table_name": "media_article" - }, - { - "id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c", - "created_date": "2025-01-20 16:04:41.483000", - "last_modified_date": "2025-01-20 16:04:41.483000", - "version": 0, - "table_name": "user" - }, - { - "id": "721a49ce-a02d-4035-add9-126a4591cbb8", - "created_date": "2025-01-20 16:04:41.271000", - "last_modified_date": "2025-01-20 16:04:41.271000", - "version": 0, - "table_name": "article" - }, - { - "id": "727a04ab-67dd-4a80-b80c-d824e41bcda2", - "created_date": "2025-01-20 16:04:41.468000", - "last_modified_date": "2025-01-20 16:04:41.468000", - "version": 0, - "table_name": "card" - }, - { - "id": "777015a8-f8b8-43b2-b285-8aae3885b7e7", - "created_date": "2025-01-20 16:04:41.047000", - "last_modified_date": "2025-01-20 16:04:41.047000", - "version": 0, - "table_name": "media_file" - }, - { - "id": "7ebdc79a-d0a7-42dc-9e50-d93c79e182af", - "created_date": "2025-01-20 16:04:41.288000", - "last_modified_date": "2025-01-20 16:04:41.288000", - "version": 0, - "table_name": "article_author" - }, - { - "id": "815b78c9-aef7-42b8-9f16-06a93a3761b3", - "created_date": "2025-01-20 16:04:41.304000", - "last_modified_date": "2025-01-20 16:04:41.304000", - "version": 0, - "table_name": "book" - }, - { - "id": "925989d3-faa9-4bb4-9183-3564b29063ec", - "created_date": "2025-01-20 16:04:41.523000", - "last_modified_date": "2025-01-20 16:04:41.523000", - "version": 0, - "table_name": "role" - }, - { - "id": "9a1a6de3-eae1-48e7-88c4-0c67908ec1bf", - "created_date": "2025-01-20 16:04:41.421000", - "last_modified_date": "2025-01-20 16:04:41.421000", - "version": 0, - "table_name": "field_position" - }, - { - "id": "a7a24f23-586b-4ac4-97ab-b610e2ab2e5d", - "created_date": "2025-01-20 16:04:41.454000", - "last_modified_date": "2025-01-20 16:04:41.454000", - "version": 0, - "table_name": "card_set" - }, - { - "id": "a9170107-b28f-4476-947e-0aaa3a2a1e01", - "created_date": "2025-01-20 16:04:41.393000", - "last_modified_date": "2025-01-20 16:04:41.393000", - "version": 0, - "table_name": "team" - }, - { - "id": "ae68f37b-9314-472c-86ea-387486f4db1c", - "created_date": "2025-01-20 16:04:41.235000", - "last_modified_date": "2025-01-20 16:04:41.235000", - "version": 0, - "table_name": "comic_work" - }, - { - "id": "b7107df0-c08d-4370-9eff-8d7ebdea14ef", - "created_date": "2025-01-20 16:04:41.216000", - "last_modified_date": "2025-01-20 16:04:41.216000", - "version": 0, - "table_name": "worktype" - }, - { - "id": "c9060971-3f7d-4eb1-89e9-bb40d8687e77", - "created_date": "2025-01-20 16:04:41.076000", - "last_modified_date": "2025-01-20 16:04:41.076000", - "version": 0, - "table_name": "artist" - }, - { - "id": "c92e35fc-b634-437a-bfa0-47d15c67fe83", - "created_date": "2025-01-20 16:04:41.548000", - "last_modified_date": "2025-01-20 16:04:41.548000", - "version": 0, - "table_name": "module_data" - }, - { - "id": "c96112de-b521-4e92-8021-0172a1aebaf8", - "created_date": "2025-01-20 16:04:41.349000", - "last_modified_date": "2025-01-20 16:04:41.349000", - "version": 0, - "table_name": "bookshelf_publisher" - }, - { - "id": "cf9b16b9-1ec0-4bed-ba95-fef65013c069", - "created_date": "2025-01-20 16:04:41.377000", - "last_modified_date": "2025-01-20 16:04:41.377000", - "version": 0, - "table_name": "player" - }, - { - "id": "d7584a1a-0249-45ed-85e9-532680643296", - "created_date": "2025-01-20 16:04:41.561000", - "last_modified_date": "2025-01-20 16:04:41.561000", - "version": 0, - "table_name": "mail_account" - }, - { - "id": "d8b65435-dacc-4129-b08a-f4588110e127", - "created_date": "2025-01-20 16:04:41.502000", - "last_modified_date": "2025-01-20 16:04:41.502000", - "version": 0, - "table_name": "token" - }, - { - "id": "e4ab0e7b-4017-4ec0-9b33-b182a1850fda", - "created_date": "2025-01-20 16:04:41.199000", - "last_modified_date": "2025-01-20 16:04:41.199000", - "version": 0, - "table_name": "story_arc" - }, - { - "id": "e86c5386-a155-4f40-a6f5-e8da635d39c4", - "created_date": "2025-01-20 16:04:41.133000", - "last_modified_date": "2025-01-20 16:04:41.133000", - "version": 0, - "table_name": "issue" - }, - { - "id": "e962f787-7795-4179-8168-b2992981da12", - "created_date": "2025-01-20 16:04:41.332000", - "last_modified_date": "2025-01-20 16:04:41.332000", - "version": 0, - "table_name": "book_author" - }, - { - "id": "ede5536c-ca16-48ec-bd0d-a3b00cc1fcab", - "created_date": "2025-01-20 16:04:41.177000", - "last_modified_date": "2025-01-20 16:04:41.177000", - "version": 0, - "table_name": "trade_paperback" - }, - { - "id": "f99eb13a-b7c6-4f51-940e-2a2c36124a67", - "created_date": "2025-01-20 16:04:41.580000", - "last_modified_date": "2025-01-20 16:04:41.580000", - "version": 0, - "table_name": "mail" - } - ], - "volume": [], - "sport": [ - { - "id": "0718122d-8eea-4710-99cf-33a1f0a9c073", - "created_date": "2024-08-16 23:58:37.150000", - "last_modified_date": "2024-08-16 23:58:37.150000", - "version": 0, - "name": "Baseball" - }, - { - "id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e", - "created_date": "2024-08-16 23:58:37.154000", - "last_modified_date": "2024-08-16 23:58:37.154000", - "version": 0, - "name": "Hockey" - }, - { - "id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6", - "created_date": "2024-08-16 23:58:37.146000", - "last_modified_date": "2024-08-16 23:58:37.146000", - "version": 0, - "name": "Football" - }, - { - "id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb", - "created_date": "2024-08-16 23:58:37.152000", - "last_modified_date": "2024-08-16 23:58:37.152000", - "version": 0, - "name": "Basketball" - } - ], - "author": [ - { - "id": "c366e7c1-7475-4229-a69d-58ced52bfaf2", - "created_date": "2024-08-16 23:58:34.692000", - "last_modified_date": "2024-08-16 23:58:34.692000", - "version": 0, - "first_name": "Douglas", - "last_name": "Adams" - } - ], - "meta_data_column": [ - { - "id": "03ee0f74-1a99-47cc-92c0-afe5d7436d83", - "created_date": "2025-01-20 16:04:41.098000", - "last_modified_date": "2025-01-21 12:07:05.587000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "33b748ee-9f55-487a-a614-2ab64b844a13" - }, - { - "id": "0437399a-4327-43e3-a3b5-a2d69597bf6b", - "created_date": "2025-01-20 16:04:41.310000", - "last_modified_date": "2025-01-21 12:07:05.922000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "815b78c9-aef7-42b8-9f16-06a93a3761b3" - }, - { - "id": "0515448a-da3b-4ef1-96f0-4befccfe9868", - "created_date": "2025-01-20 16:04:41.163000", - "last_modified_date": "2025-01-21 12:07:05.685000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "13ea1a84-f92e-4ce6-b709-fe7c1893d229" - }, - { - "id": "05465284-7be3-40db-bae7-696d5a86fa60", - "created_date": "2025-01-20 16:04:41.328000", - "last_modified_date": "2025-01-21 12:07:05.941000", - "version": 1, - "column_name": "publisher_id", - "column_sync_name": "publisher_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 8, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "815b78c9-aef7-42b8-9f16-06a93a3761b3" - }, - { - "id": "055b3031-de10-4d50-8ca7-8784b65edebb", - "created_date": "2025-01-20 16:04:41.170000", - "last_modified_date": "2025-01-21 12:07:05.696000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "13ea1a84-f92e-4ce6-b709-fe7c1893d229" - }, - { - "id": "05cd24b6-f9d1-4ee6-b39c-4ce3e9b2a512", - "created_date": "2025-01-20 16:04:41.355000", - "last_modified_date": "2025-01-21 12:07:05.976000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "c96112de-b521-4e92-8021-0172a1aebaf8" - }, - { - "id": "06072c98-d51c-428f-a6c9-5d2ddca99a21", - "created_date": "2025-01-20 16:04:41.222000", - "last_modified_date": "2025-01-21 12:07:05.775000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "b7107df0-c08d-4370-9eff-8d7ebdea14ef" - }, - { - "id": "07069490-9d03-49d4-a4bb-eb43b71e65cf", - "created_date": "2025-01-20 16:04:41.341000", - "last_modified_date": "2025-01-21 12:07:05.957000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "e962f787-7795-4179-8168-b2992981da12" - }, - { - "id": "072c1188-bbe5-4e1c-bb8d-4fef313fdbf6", - "created_date": "2025-01-20 16:04:41.458000", - "last_modified_date": "2025-01-21 12:07:06.143000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "a7a24f23-586b-4ac4-97ab-b610e2ab2e5d" - }, - { - "id": "07eaf26b-4d49-4008-bf4f-5901b30c4d13", - "created_date": "2025-01-20 16:04:41.478000", - "last_modified_date": "2025-01-21 12:07:06.189000", - "version": 1, - "column_name": "card_set_id", - "column_sync_name": "card_set_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 7, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "727a04ab-67dd-4a80-b80c-d824e41bcda2" - }, - { - "id": "0896813d-8a00-41ca-8656-b9ce149897c9", - "created_date": "2025-01-20 16:04:41.228000", - "last_modified_date": "2025-01-21 12:07:05.783000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "b7107df0-c08d-4370-9eff-8d7ebdea14ef" - }, - { - "id": "089ea4df-d724-42da-a5d6-896aa4ba1df1", - "created_date": "2025-01-20 16:04:41.611000", - "last_modified_date": "2025-01-21 12:07:06.429000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" - }, - { - "id": "08ce1340-51b6-4556-99d1-9817637fe9ab", - "created_date": "2025-01-20 16:04:41.583000", - "last_modified_date": "2025-01-21 12:07:06.378000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "f99eb13a-b7c6-4f51-940e-2a2c36124a67" - }, - { - "id": "0a48118a-ca4f-4043-8d56-39a439af877f", - "created_date": "2025-01-20 16:04:41.122000", - "last_modified_date": "2025-01-21 12:07:05.626000", - "version": 1, - "column_name": "current_order", - "column_sync_name": "current_order", - "column_type": "BOOLEAN", - "column_modifier": null, - "column_order": 6, - "is_shown": 1, - "column_label": "Bestellung", - "show_filter": 1, - "filter_label": "Bestellung", - "ref_column": null, - "table_id": "26478de9-0bcc-427f-ba60-0a1ab79e107b" - }, - { - "id": "0a694da0-45ff-4604-826a-ed67a5a6b0c5", - "created_date": "2025-01-20 16:04:41.487000", - "last_modified_date": "2025-01-21 12:07:06.209000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c" - }, - { - "id": "0a87d4ec-fe51-484c-8203-3626af24edcc", - "created_date": "2025-01-20 16:04:41.267000", - "last_modified_date": "2025-01-21 12:07:05.856000", - "version": 1, - "column_name": "last_name", - "column_sync_name": "last_name", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 6, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "1951f2b6-3640-4568-9c47-a20e0768a843" - }, - { - "id": "0bbab597-06e3-417e-84a9-0a588d5854a3", - "created_date": "2025-01-20 16:04:40.988000", - "last_modified_date": "2025-01-21 12:07:05.319000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 1, - "column_label": "ID", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "63285ce5-eb45-47f5-9575-dec1f9778bbc" - }, - { - "id": "0bcbc19e-d442-429c-8640-ca8513c62fa6", - "created_date": "2025-01-20 16:04:41.462000", - "last_modified_date": "2025-01-21 12:07:06.154000", - "version": 1, - "column_name": "parallel_set", - "column_sync_name": "parallel_set", - "column_type": "BOOLEAN", - "column_modifier": null, - "column_order": 6, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "a7a24f23-586b-4ac4-97ab-b610e2ab2e5d" - }, - { - "id": "0c1c8451-d52d-4ef2-8b7b-fecd689bb702", - "created_date": "2025-01-20 16:04:41.109000", - "last_modified_date": "2025-01-21 12:07:05.602000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 1, - "column_label": "ID", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "26478de9-0bcc-427f-ba60-0a1ab79e107b" - }, - { - "id": "0d572856-2796-40e5-b6cf-05ffde91ee67", - "created_date": "2025-01-20 16:04:41.472000", - "last_modified_date": "2025-01-21 12:07:06.174000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "727a04ab-67dd-4a80-b80c-d824e41bcda2" - }, - { - "id": "0ea1f9d0-217d-4037-a9d6-d7624cc909ea", - "created_date": "2025-01-20 16:04:41.627000", - "last_modified_date": "2025-01-21 12:07:06.464000", - "version": 1, - "column_name": "filter_label", - "column_sync_name": "filter_label", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 13, - "is_shown": 1, - "column_label": "Filter Label", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" - }, - { - "id": "0f599c2f-f967-4869-9cce-7523e820017a", - "created_date": "2025-01-20 16:04:41.238000", - "last_modified_date": "2025-01-21 12:07:05.802000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "ae68f37b-9314-472c-86ea-387486f4db1c" - }, - { - "id": "1008557e-bf76-4b6e-bd25-62e025505ae0", - "created_date": "2025-01-20 16:04:41.026000", - "last_modified_date": "2025-01-21 12:07:05.452000", - "version": 1, - "column_name": "review", - "column_sync_name": "review", - "column_type": "BOOLEAN", - "column_modifier": null, - "column_order": 6, - "is_shown": 1, - "column_label": "Review", - "show_filter": 1, - "filter_label": "Review", - "ref_column": null, - "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" - }, - { - "id": "1034d208-8467-418a-9604-49d1047115a6", - "created_date": "2025-01-20 16:04:41.012000", - "last_modified_date": "2025-01-21 12:07:05.420000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 1, - "column_label": "ID", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" - }, - { - "id": "119dce89-209b-4557-92e0-9aab2d704358", - "created_date": "2025-01-20 16:04:41.343000", - "last_modified_date": "2025-01-21 12:07:05.960000", - "version": 1, - "column_name": "author_id", - "column_sync_name": "author_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "e962f787-7795-4179-8168-b2992981da12" - }, - { - "id": "1222fd06-05f8-48b4-9d8c-7f17b87153ab", - "created_date": "2025-01-20 16:04:41.590000", - "last_modified_date": "2025-01-21 12:07:06.391000", - "version": 1, - "column_name": "subject", - "column_sync_name": "subject", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 6, - "is_shown": 1, - "column_label": "Subject", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "f99eb13a-b7c6-4f51-940e-2a2c36124a67" - }, - { - "id": "14316703-5291-4b46-8cc2-0aa0d65ddc98", - "created_date": "2025-01-20 16:04:41.366000", - "last_modified_date": "2025-01-21 12:07:05.992000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "1850a718-dd07-4766-9856-591d311a4d0d" - }, - { - "id": "14eeb89e-ab64-4d0a-a079-ada87dba55b4", - "created_date": "2025-01-20 16:04:41.549000", - "last_modified_date": "2025-01-21 12:07:06.309000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 1, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "c92e35fc-b634-437a-bfa0-47d15c67fe83" - }, - { - "id": "15c5eaa1-03db-4b05-8931-906d8425e945", - "created_date": "2025-01-20 16:04:41.191000", - "last_modified_date": "2025-01-21 12:07:05.732000", - "version": 1, - "column_name": "issue_end", - "column_sync_name": "issue_end", - "column_type": "LONG", - "column_modifier": null, - "column_order": 6, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "ede5536c-ca16-48ec-bd0d-a3b00cc1fcab" - }, - { - "id": "179794a0-99b1-48c3-8ffd-e64d5fa9839b", - "created_date": "2025-01-20 16:04:41.368000", - "last_modified_date": "2025-01-21 12:07:05.996000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "1850a718-dd07-4766-9856-591d311a4d0d" - }, - { - "id": "17b378d0-ec86-481e-bd55-98f613895df1", - "created_date": "2025-01-20 16:04:41.616000", - "last_modified_date": "2025-01-21 12:07:06.442000", - "version": 1, - "column_name": "column_sync_name", - "column_sync_name": "column_sync_name", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 6, - "is_shown": 1, - "column_label": "SQLite Column", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" - }, - { - "id": "19dbe5fa-c37e-44a7-aa0a-8a514006b296", - "created_date": "2025-01-20 16:04:41.225000", - "last_modified_date": "2025-01-21 12:07:05.779000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "b7107df0-c08d-4370-9eff-8d7ebdea14ef" - }, - { - "id": "1a8abf9c-614b-4f0c-9ed4-bfd4e7763ac5", - "created_date": "2025-01-20 16:04:41.474000", - "last_modified_date": "2025-01-21 12:07:06.178000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "727a04ab-67dd-4a80-b80c-d824e41bcda2" - }, - { - "id": "1b97de61-a7a6-4254-a057-c5297e51b42f", - "created_date": "2025-01-20 16:04:41.434000", - "last_modified_date": "2025-01-21 12:07:06.099000", - "version": 1, - "column_name": "sport_id", - "column_sync_name": "sport_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 7, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "9a1a6de3-eae1-48e7-88c4-0c67908ec1bf" - }, - { - "id": "1d2628dd-d89a-4190-afc9-382fc47b9322", - "created_date": "2025-01-20 16:04:41.498000", - "last_modified_date": "2025-01-21 12:07:06.229000", - "version": 1, - "column_name": "password", - "column_sync_name": "password", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 9, - "is_shown": 0, - "column_label": "Password", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c" - }, - { - "id": "1e4609b3-3e47-41bd-a2e9-826a9db1bdb7", - "created_date": "2025-01-20 16:04:41.471000", - "last_modified_date": "2025-01-21 12:07:06.170000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "727a04ab-67dd-4a80-b80c-d824e41bcda2" - }, - { - "id": "20809d98-c875-49d3-b37c-993db1d1eb0e", - "created_date": "2025-01-20 16:04:41.500000", - "last_modified_date": "2025-01-21 12:07:06.233000", - "version": 1, - "column_name": "enabled", - "column_sync_name": "enabled", - "column_type": "BOOLEAN", - "column_modifier": null, - "column_order": 10, - "is_shown": 1, - "column_label": "", - "show_filter": 1, - "filter_label": null, - "ref_column": null, - "table_id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c" - }, - { - "id": "2144deeb-23f6-4f56-9407-6747c5df7d6c", - "created_date": "2025-01-20 16:04:41.402000", - "last_modified_date": "2025-01-21 12:07:06.048000", - "version": 1, - "column_name": "name", - "column_sync_name": "name", - "column_type": "TEXT", - "column_modifier": "UNIQUE", - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "a9170107-b28f-4476-947e-0aaa3a2a1e01" - }, - { - "id": "236f7302-a1af-4461-a147-2f23b9894910", - "created_date": "2025-01-20 16:04:41.279000", - "last_modified_date": "2025-01-21 12:07:05.871000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "721a49ce-a02d-4035-add9-126a4591cbb8" - }, - { - "id": "23fee615-37b2-4e69-846c-515b78aeef28", - "created_date": "2025-01-20 16:04:41.624000", - "last_modified_date": "2025-01-21 12:07:06.458000", - "version": 1, - "column_name": "column_label", - "column_sync_name": "column_label", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 11, - "is_shown": 1, - "column_label": "Label", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" - }, - { - "id": "261eb1f8-d845-44a8-a6a9-70957f893409", - "created_date": "2025-01-20 16:04:41.427000", - "last_modified_date": "2025-01-21 12:07:06.086000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "9a1a6de3-eae1-48e7-88c4-0c67908ec1bf" - }, - { - "id": "27a6fceb-dc77-4a9c-b897-8cc11bead54a", - "created_date": "2025-01-20 16:04:41.568000", - "last_modified_date": "2025-01-21 12:07:06.344000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "d7584a1a-0249-45ed-85e9-532680643296" - }, - { - "id": "2a601ddc-6fbe-4402-8033-ce42adf0a4fa", - "created_date": "2025-01-20 16:04:41.244000", - "last_modified_date": "2025-01-21 12:07:05.812000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "ae68f37b-9314-472c-86ea-387486f4db1c" - }, - { - "id": "2ea9583e-c19f-4692-a2dd-578ad5b6553d", - "created_date": "2025-01-20 16:04:41.250000", - "last_modified_date": "2025-01-21 12:07:05.824000", - "version": 1, - "column_name": "comic_id", - "column_sync_name": "comic_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 6, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "ae68f37b-9314-472c-86ea-387486f4db1c" - }, - { - "id": "2fd476b2-0df4-4b12-836e-babfcacb28e4", - "created_date": "2025-01-20 16:04:41.334000", - "last_modified_date": "2025-01-21 12:07:05.947000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "e962f787-7795-4179-8168-b2992981da12" - }, - { - "id": "321270c6-366d-4713-9f92-f78dc3e903ce", - "created_date": "2025-01-20 16:04:41.388000", - "last_modified_date": "2025-01-21 12:07:06.026000", - "version": 1, - "column_name": "first_name", - "column_sync_name": "first_name", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "cf9b16b9-1ec0-4bed-ba95-fef65013c069" - }, - { - "id": "3334c5c2-cf01-459b-b021-67cfcad9dc40", - "created_date": "2025-01-20 16:04:41.578000", - "last_modified_date": "2025-01-21 12:07:06.369000", - "version": 1, - "column_name": "start_tls", - "column_sync_name": "start_tls", - "column_type": "BOOLEAN", - "column_modifier": null, - "column_order": 10, - "is_shown": 1, - "column_label": "StartTLS", - "show_filter": 1, - "filter_label": "StartTLS", - "ref_column": null, - "table_id": "d7584a1a-0249-45ed-85e9-532680643296" - }, - { - "id": "3351c7ff-bf86-41c7-b426-c90a071601fc", - "created_date": "2025-01-20 16:04:40.997000", - "last_modified_date": "2025-01-21 12:07:05.386000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "63285ce5-eb45-47f5-9575-dec1f9778bbc" - }, - { - "id": "3453a28e-1fad-45b7-bec9-e125b1d70ef5", - "created_date": "2025-01-20 16:04:41.231000", - "last_modified_date": "2025-01-21 12:07:05.795000", - "version": 1, - "column_name": "name", - "column_sync_name": "name", - "column_type": "TEXT", - "column_modifier": "UNIQUE", - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "b7107df0-c08d-4370-9eff-8d7ebdea14ef" - }, - { - "id": "34d54638-7820-4943-9026-a685a5c65942", - "created_date": "2025-01-20 16:04:41.564000", - "last_modified_date": "2025-01-21 12:07:06.337000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "d7584a1a-0249-45ed-85e9-532680643296" - }, - { - "id": "355a64ce-5196-45cd-9915-c656a96ed2c6", - "created_date": "2025-01-20 16:04:41.339000", - "last_modified_date": "2025-01-21 12:07:05.954000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "e962f787-7795-4179-8168-b2992981da12" - }, - { - "id": "361c9a8d-2a9a-4662-97f6-612dd90eedb8", - "created_date": "2025-01-20 16:04:41.602000", - "last_modified_date": "2025-01-21 12:07:06.414000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "058fa05e-2dab-4f61-8415-b5beec1fe79e" - }, - { - "id": "365ce980-90ba-4d3e-befd-956467ce3f54", - "created_date": "2025-01-20 16:04:41.609000", - "last_modified_date": "2025-01-21 12:07:06.426000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" - }, - { - "id": "36f94e1b-601d-42f7-90c0-c19f4bf63d91", - "created_date": "2025-01-20 16:04:41.492000", - "last_modified_date": "2025-01-21 12:07:06.212000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c" - }, - { - "id": "3881219c-8329-4f1c-b81d-86b97a3ba0b2", - "created_date": "2025-01-20 16:04:41.485000", - "last_modified_date": "2025-01-21 12:07:06.202000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 1, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c" - }, - { - "id": "398c17e5-a694-408c-81df-beaedad99486", - "created_date": "2025-01-20 16:04:41.538000", - "last_modified_date": "2025-01-21 12:07:06.287000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 1, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "36c7ac9c-270d-40a4-af43-c952120f165c" - }, - { - "id": "3bf5a99b-3d42-4e7f-958f-1abe357d8fcb", - "created_date": "2025-01-20 16:04:41.068000", - "last_modified_date": "2025-01-21 12:07:05.535000", - "version": 1, - "column_name": "file_name", - "column_sync_name": "file_name", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 9, - "is_shown": 1, - "column_label": "Dateiname", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" - }, - { - "id": "3c5e32ae-8b77-4173-8f80-fa458da7f8b0", - "created_date": "2025-01-20 16:04:41.072000", - "last_modified_date": "2025-01-21 12:07:05.545000", - "version": 1, - "column_name": "cloud_link", - "column_sync_name": "cloud_link", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 11, - "is_shown": 1, - "column_label": "Cloud Link", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" - }, - { - "id": "3d249be9-3990-451d-8665-03855a3b06c5", - "created_date": "2025-01-20 16:04:41.413000", - "last_modified_date": "2025-01-21 12:07:06.063000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "3773ac76-a957-42b2-b24d-5e66e4c32c12" - }, - { - "id": "3dc0ea2d-7367-4b65-804a-4e63758c09a7", - "created_date": "2025-01-20 16:04:41.180000", - "last_modified_date": "2025-01-21 12:07:05.710000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "ede5536c-ca16-48ec-bd0d-a3b00cc1fcab" - }, - { - "id": "41673309-d4b3-44fc-a7b0-7f13d535d49f", - "created_date": "2025-01-20 16:04:41.142000", - "last_modified_date": "2025-01-21 12:07:05.652000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "e86c5386-a155-4f40-a6f5-e8da635d39c4" - }, - { - "id": "41ce422a-fa89-4b0c-8e50-d28cbe8a285f", - "created_date": "2025-01-20 16:04:41.599000", - "last_modified_date": "2025-01-21 12:07:06.408000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 1, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "058fa05e-2dab-4f61-8415-b5beec1fe79e" - }, - { - "id": "4247146c-8911-441e-aac9-ef1bcb73eb07", - "created_date": "2025-01-20 16:04:41.174000", - "last_modified_date": "2025-01-21 12:07:05.704000", - "version": 1, - "column_name": "comic_id", - "column_sync_name": "comic_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 6, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "13ea1a84-f92e-4ce6-b709-fe7c1893d229" - }, - { - "id": "42c4d72b-742e-4692-9797-c94e5f5b91bf", - "created_date": "2025-01-20 16:04:41.201000", - "last_modified_date": "2025-01-21 12:07:05.746000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "e4ab0e7b-4017-4ec0-9b33-b182a1850fda" - }, - { - "id": "432204a9-1f00-4a4f-ad8c-fe051ab84299", - "created_date": "2025-01-20 16:04:41.150000", - "last_modified_date": "2025-01-21 12:07:05.665000", - "version": 1, - "column_name": "is_read", - "column_sync_name": "is_read", - "column_type": "BOOLEAN", - "column_modifier": null, - "column_order": 6, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "e86c5386-a155-4f40-a6f5-e8da635d39c4" - }, - { - "id": "43d855fc-15e2-4a87-b7b3-3c43f0091f9f", - "created_date": "2025-01-20 16:04:41.540000", - "last_modified_date": "2025-01-21 12:07:06.291000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "36c7ac9c-270d-40a4-af43-c952120f165c" - }, - { - "id": "4488149c-3dba-43c1-a925-101c42fbd7cf", - "created_date": "2025-01-20 16:04:41.085000", - "last_modified_date": "2025-01-21 12:07:05.568000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "c9060971-3f7d-4eb1-89e9-bb40d8687e77" - }, - { - "id": "46db6f22-7ae4-477f-ad66-e0e8783bdd8e", - "created_date": "2025-01-20 16:04:41.001000", - "last_modified_date": "2025-01-21 12:07:05.398000", - "version": 1, - "column_name": "url", - "column_sync_name": "link_url", - "column_type": "TEXT", - "column_modifier": "UNIQUE", - "column_order": 5, - "is_shown": 1, - "column_label": "URL", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "63285ce5-eb45-47f5-9575-dec1f9778bbc" - }, - { - "id": "47283da8-4f49-4383-a755-2dad75ceaa6e", - "created_date": "2025-01-20 16:04:41.325000", - "last_modified_date": "2025-01-21 12:07:05.938000", - "version": 1, - "column_name": "year", - "column_sync_name": "year", - "column_type": "LONG", - "column_modifier": null, - "column_order": 7, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "815b78c9-aef7-42b8-9f16-06a93a3761b3" - }, - { - "id": "47f5e390-9bcf-4b3d-9470-69dac67bd504", - "created_date": "2025-01-20 16:04:41.261000", - "last_modified_date": "2025-01-21 12:07:05.843000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "1951f2b6-3640-4568-9c47-a20e0768a843" - }, - { - "id": "48032d74-a203-416b-b8a4-35e005fc5cb1", - "created_date": "2025-01-20 16:04:41.476000", - "last_modified_date": "2025-01-21 12:07:06.185000", - "version": 1, - "column_name": "year", - "column_sync_name": "year", - "column_type": "LONG", - "column_modifier": null, - "column_order": 6, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "727a04ab-67dd-4a80-b80c-d824e41bcda2" - }, - { - "id": "487fae4f-0987-4c64-8c8a-44b96c9e1ace", - "created_date": "2025-01-20 16:04:41.274000", - "last_modified_date": "2025-01-21 12:07:05.864000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "721a49ce-a02d-4035-add9-126a4591cbb8" - }, - { - "id": "48e670ae-95de-402d-a800-6e568ab0e12b", - "created_date": "2025-01-20 16:04:41.052000", - "last_modified_date": "2025-01-21 12:07:05.499000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "Created", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" - }, - { - "id": "497a41e6-305e-48a4-baf4-f0d86feb33e9", - "created_date": "2025-01-20 16:04:41.056000", - "last_modified_date": "2025-01-21 12:07:05.509000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "Version", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" - }, - { - "id": "49845f27-8d26-4dd2-b139-c64d1d6e5147", - "created_date": "2025-01-20 16:04:41.527000", - "last_modified_date": "2025-01-21 12:07:06.273000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "925989d3-faa9-4bb4-9183-3564b29063ec" - }, - { - "id": "498559d5-13cf-4ef9-b0bc-9304ac84a352", - "created_date": "2025-01-20 16:04:41.596000", - "last_modified_date": "2025-01-21 12:07:06.402000", - "version": 1, - "column_name": "received_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 9, - "is_shown": 0, - "column_label": "Empfangen", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "f99eb13a-b7c6-4f51-940e-2a2c36124a67" - }, - { - "id": "49bf7ec6-2583-4d7e-a91c-2971dd3a7dd6", - "created_date": "2025-01-20 16:04:41.442000", - "last_modified_date": "2025-01-21 12:07:06.108000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "3ce78ef2-35c6-4397-8cdf-9241b52236df" - }, - { - "id": "4a414632-df81-41c2-b85d-70ec2f71dcac", - "created_date": "2025-01-20 16:04:41.615000", - "last_modified_date": "2025-01-21 12:07:06.438000", - "version": 1, - "column_name": "column_name", - "column_sync_name": "column_name", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 5, - "is_shown": 1, - "column_label": "Column", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" - }, - { - "id": "4a9506f7-4fc7-45ed-b570-d7f32bfe2dcc", - "created_date": "2025-01-20 16:04:41.466000", - "last_modified_date": "2025-01-21 12:07:06.161000", - "version": 1, - "column_name": "vendor_id", - "column_sync_name": "vendor_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 8, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "a7a24f23-586b-4ac4-97ab-b610e2ab2e5d" - }, - { - "id": "4b442acc-3d9b-4d4e-906b-49fb1de52e4e", - "created_date": "2025-01-20 16:04:40.999000", - "last_modified_date": "2025-01-21 12:07:05.392000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "63285ce5-eb45-47f5-9575-dec1f9778bbc" - }, - { - "id": "4db2fa33-64e6-4596-a1b2-9fdc657dd3b3", - "created_date": "2025-01-20 16:04:41.423000", - "last_modified_date": "2025-01-21 12:07:06.079000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "9a1a6de3-eae1-48e7-88c4-0c67908ec1bf" - }, - { - "id": "4dc7467e-8707-46fe-9478-071c7006b567", - "created_date": "2025-01-20 16:04:41.252000", - "last_modified_date": "2025-01-21 12:07:05.829000", - "version": 1, - "column_name": "work_type_id", - "column_sync_name": "work_type_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 7, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "ae68f37b-9314-472c-86ea-387486f4db1c" - }, - { - "id": "4dd473c3-1850-4c0a-90ca-34084cf17420", - "created_date": "2025-01-20 16:04:41.042000", - "last_modified_date": "2025-01-21 12:07:05.484000", - "version": 1, - "column_name": "cloud_link", - "column_sync_name": "cloud_link", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 11, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" - }, - { - "id": "4e3940e5-b899-4667-a6f5-cc206cf030d8", - "created_date": "2025-01-20 16:04:41.507000", - "last_modified_date": "2025-01-21 12:07:06.245000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "d8b65435-dacc-4129-b08a-f4588110e127" - }, - { - "id": "511620ef-4bce-4d28-bcfa-5d94a1b979ba", - "created_date": "2025-01-20 16:04:41.298000", - "last_modified_date": "2025-01-21 12:07:05.903000", - "version": 1, - "column_name": "article_id", - "column_sync_name": "article_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "7ebdc79a-d0a7-42dc-9e50-d93c79e182af" - }, - { - "id": "5276d5cc-94a7-4840-ab2a-5daec758d7b2", - "created_date": "2025-01-20 16:04:41.005000", - "last_modified_date": "2025-01-21 12:07:05.410000", - "version": 1, - "column_name": "title", - "column_sync_name": "title", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 7, - "is_shown": 1, - "column_label": "Title", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "63285ce5-eb45-47f5-9575-dec1f9778bbc" - }, - { - "id": "52fd2cc1-d77d-4fbd-bde9-55c522de4a46", - "created_date": "2025-01-20 16:04:41.308000", - "last_modified_date": "2025-01-21 12:07:05.917000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "815b78c9-aef7-42b8-9f16-06a93a3761b3" - }, - { - "id": "55cb2c7f-b93e-4cbe-9886-6f035ab57a5f", - "created_date": "2025-01-20 16:04:41.152000", - "last_modified_date": "2025-01-21 12:07:05.669000", - "version": 1, - "column_name": "issue_number", - "column_sync_name": "issue_number", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 7, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "e86c5386-a155-4f40-a6f5-e8da635d39c4" - }, - { - "id": "568d6d6a-f9df-40a6-8553-17480040a2fa", - "created_date": "2025-01-20 16:04:41.518000", - "last_modified_date": "2025-01-21 12:07:06.263000", - "version": 1, - "column_name": "enabled", - "column_sync_name": "enabled", - "column_type": "BOOLEAN", - "column_modifier": null, - "column_order": 8, - "is_shown": 1, - "column_label": "", - "show_filter": 1, - "filter_label": "Enabled", - "ref_column": null, - "table_id": "d8b65435-dacc-4129-b08a-f4588110e127" - }, - { - "id": "56f983ed-4d38-4a2f-89b8-c042aeeef043", - "created_date": "2025-01-20 16:04:41.440000", - "last_modified_date": "2025-01-21 12:07:06.104000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "3ce78ef2-35c6-4397-8cdf-9241b52236df" - }, - { - "id": "57c6ac6b-50f7-40f1-b809-fcafaae5bb34", - "created_date": "2025-01-20 16:04:41.604000", - "last_modified_date": "2025-01-21 12:07:06.420000", - "version": 1, - "column_name": "table_name", - "column_sync_name": "table_name", - "column_type": "TEXT", - "column_modifier": "UNIQUE", - "column_order": 5, - "is_shown": 1, - "column_label": "Table", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "058fa05e-2dab-4f61-8415-b5beec1fe79e" - }, - { - "id": "57e8ebb5-b8a7-49fb-b74a-2bf1e65f4cb6", - "created_date": "2025-01-20 16:04:41.219000", - "last_modified_date": "2025-01-21 12:07:05.772000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "b7107df0-c08d-4370-9eff-8d7ebdea14ef" - }, - { - "id": "581501f7-9dc5-44df-b1a3-74f930db7c1b", - "created_date": "2025-01-20 16:04:41.373000", - "last_modified_date": "2025-01-21 12:07:06.005000", - "version": 1, - "column_name": "name", - "column_sync_name": "name", - "column_type": "TEXT", - "column_modifier": "UNIQUE", - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "1850a718-dd07-4766-9856-591d311a4d0d" - }, - { - "id": "5a97f0ea-c91c-49ae-b682-86807aedfdd7", - "created_date": "2025-01-20 16:04:41.455000", - "last_modified_date": "2025-01-21 12:07:06.136000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "a7a24f23-586b-4ac4-97ab-b610e2ab2e5d" - }, - { - "id": "5aeb10c3-8732-46a7-8a13-9385a183ba44", - "created_date": "2025-01-20 16:04:41.188000", - "last_modified_date": "2025-01-21 12:07:05.728000", - "version": 1, - "column_name": "issue_start", - "column_sync_name": "issue_start", - "column_type": "LONG", - "column_modifier": null, - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "ede5536c-ca16-48ec-bd0d-a3b00cc1fcab" - }, - { - "id": "5c99ea74-f722-4c0e-8148-ff9d9684570a", - "created_date": "2025-01-20 16:04:41.525000", - "last_modified_date": "2025-01-21 12:07:06.271000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 1, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "925989d3-faa9-4bb4-9183-3564b29063ec" - }, - { - "id": "5dec99e7-0066-41cb-aa8e-fd5abaa3622d", - "created_date": "2025-01-20 16:04:41.603000", - "last_modified_date": "2025-01-21 12:07:06.417000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "058fa05e-2dab-4f61-8415-b5beec1fe79e" - }, - { - "id": "5eb89fb4-636c-4721-a931-fd4ed7940d10", - "created_date": "2025-01-20 16:04:41.591000", - "last_modified_date": "2025-01-21 12:07:06.395000", - "version": 1, - "column_name": "body", - "column_sync_name": "body", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 7, - "is_shown": 0, - "column_label": "Body", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "f99eb13a-b7c6-4f51-940e-2a2c36124a67" - }, - { - "id": "5f18a8a4-dca2-4eed-b45c-bd8b2013cfd3", - "created_date": "2025-01-20 16:04:41.066000", - "last_modified_date": "2025-01-21 12:07:05.530000", - "version": 1, - "column_name": "title", - "column_sync_name": "title", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 8, - "is_shown": 1, - "column_label": "Title", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" - }, - { - "id": "5f267ab0-669b-4544-aad8-8a9dd0dbd73b", - "created_date": "2025-01-20 16:04:41.022000", - "last_modified_date": "2025-01-21 12:07:05.446000", - "version": 1, - "column_name": "url", - "column_sync_name": "link_url", - "column_type": "TEXT", - "column_modifier": "UNIQUE", - "column_order": 5, - "is_shown": 1, - "column_label": "URL", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" - }, - { - "id": "608efc2a-4d08-4dd4-96c9-bacc37314dba", - "created_date": "2025-01-20 16:04:41.406000", - "last_modified_date": "2025-01-21 12:07:06.054000", - "version": 1, - "column_name": "sport_id", - "column_sync_name": "sport_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 7, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "a9170107-b28f-4476-947e-0aaa3a2a1e01" - }, - { - "id": "616ee1c5-3da5-48a3-baa0-d7160b8db180", - "created_date": "2025-01-20 16:04:41.306000", - "last_modified_date": "2025-01-21 12:07:05.913000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "815b78c9-aef7-42b8-9f16-06a93a3761b3" - }, - { - "id": "623d3bf2-1615-4331-ad79-13bf523cc154", - "created_date": "2025-01-20 16:04:41.138000", - "last_modified_date": "2025-01-21 12:07:05.647000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "e86c5386-a155-4f40-a6f5-e8da635d39c4" - }, - { - "id": "6295b2f3-72e7-43b4-aed7-93d3c6ad86be", - "created_date": "2025-01-20 16:04:41.625000", - "last_modified_date": "2025-01-21 12:07:06.461000", - "version": 1, - "column_name": "show_filter", - "column_sync_name": "show_filter", - "column_type": "BOOLEAN", - "column_modifier": null, - "column_order": 12, - "is_shown": 1, - "column_label": "Show Filter", - "show_filter": 1, - "filter_label": "Show Filter", - "ref_column": null, - "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" - }, - { - "id": "62aa6fe8-1111-4597-af70-db709553fb3c", - "created_date": "2025-01-20 16:04:41.432000", - "last_modified_date": "2025-01-21 12:07:06.095000", - "version": 1, - "column_name": "short_name", - "column_sync_name": "short_name", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 6, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "9a1a6de3-eae1-48e7-88c4-0c67908ec1bf" - }, - { - "id": "634ecab0-a1a4-4baa-8437-af4522b51ef1", - "created_date": "2025-01-20 16:04:41.165000", - "last_modified_date": "2025-01-21 12:07:05.689000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "13ea1a84-f92e-4ce6-b709-fe7c1893d229" - }, - { - "id": "63991f2e-49ff-4293-a8d6-b1287bb80975", - "created_date": "2025-01-20 16:04:41.400000", - "last_modified_date": "2025-01-21 12:07:06.045000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "a9170107-b28f-4476-947e-0aaa3a2a1e01" - }, - { - "id": "63e5d2bc-dbca-4731-8f0e-5829749e1fe4", - "created_date": "2025-01-20 16:04:41.353000", - "last_modified_date": "2025-01-21 12:07:05.973000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "c96112de-b521-4e92-8021-0172a1aebaf8" - }, - { - "id": "6422a660-c56b-4aa8-a371-7ace8f659e5c", - "created_date": "2025-01-20 16:04:41.364000", - "last_modified_date": "2025-01-21 12:07:05.988000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "1850a718-dd07-4766-9856-591d311a4d0d" - }, - { - "id": "6644fc7d-ce34-427c-bd32-37d2df93a9af", - "created_date": "2025-01-20 16:04:41.322000", - "last_modified_date": "2025-01-21 12:07:05.933000", - "version": 1, - "column_name": "title", - "column_sync_name": "title", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 6, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "815b78c9-aef7-42b8-9f16-06a93a3761b3" - }, - { - "id": "66a6de00-e088-4ece-a223-a37193c51aca", - "created_date": "2025-01-20 16:04:41.315000", - "last_modified_date": "2025-01-21 12:07:05.930000", - "version": 1, - "column_name": "isbn", - "column_sync_name": "isbn", - "column_type": "TEXT", - "column_modifier": "UNIQUE", - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "815b78c9-aef7-42b8-9f16-06a93a3761b3" - }, - { - "id": "682c7c9c-843e-4b2c-8e1e-1973e1ffbe5d", - "created_date": "2025-01-20 16:04:41.404000", - "last_modified_date": "2025-01-21 12:07:06.051000", - "version": 1, - "column_name": "short_name", - "column_sync_name": "short_name", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 6, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "a9170107-b28f-4476-947e-0aaa3a2a1e01" - }, - { - "id": "686e4b0a-b5ab-473a-8e0b-b4401e196039", - "created_date": "2025-01-20 16:04:41.532000", - "last_modified_date": "2025-01-21 12:07:06.282000", - "version": 1, - "column_name": "name", - "column_sync_name": "name", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 5, - "is_shown": 1, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "925989d3-faa9-4bb4-9183-3564b29063ec" - }, - { - "id": "6a46aa6b-749e-4985-b491-0b06cb78e77b", - "created_date": "2025-01-20 16:04:41.102000", - "last_modified_date": "2025-01-21 12:07:05.595000", - "version": 1, - "column_name": "name", - "column_sync_name": "name", - "column_type": "TEXT", - "column_modifier": "UNIQUE", - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "33b748ee-9f55-487a-a614-2ab64b844a13" - }, - { - "id": "6a974ed1-b29d-4f6f-a309-bcb8ede29430", - "created_date": "2025-01-20 16:04:41.554000", - "last_modified_date": "2025-01-21 12:07:06.321000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "c92e35fc-b634-437a-bfa0-47d15c67fe83" - }, - { - "id": "6c46f5e1-0d9f-4706-bcf3-2c3678c5857b", - "created_date": "2025-01-20 16:04:41.546000", - "last_modified_date": "2025-01-21 12:07:06.305000", - "version": 1, - "column_name": "role_id", - "column_sync_name": "role_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 6, - "is_shown": 1, - "column_label": "Role", - "show_filter": 0, - "filter_label": null, - "ref_column": "name", - "table_id": "36c7ac9c-270d-40a4-af43-c952120f165c" - }, - { - "id": "6cc594e1-851b-4a0f-b124-47fe6b349dcf", - "created_date": "2025-01-20 16:04:41.114000", - "last_modified_date": "2025-01-21 12:07:05.611000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "26478de9-0bcc-427f-ba60-0a1ab79e107b" - }, - { - "id": "6cd374d1-610f-4924-b767-c139a6582ca4", - "created_date": "2025-01-20 16:04:41.312000", - "last_modified_date": "2025-01-21 12:07:05.925000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "815b78c9-aef7-42b8-9f16-06a93a3761b3" - }, - { - "id": "6d22c2b2-5aab-4f1c-a18b-8dc29fbff475", - "created_date": "2025-01-20 16:04:41.246000", - "last_modified_date": "2025-01-21 12:07:05.816000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "ae68f37b-9314-472c-86ea-387486f4db1c" - }, - { - "id": "6e5e1357-cb14-499b-a624-20ceb6a363f4", - "created_date": "2025-01-20 16:04:41.116000", - "last_modified_date": "2025-01-21 12:07:05.616000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "26478de9-0bcc-427f-ba60-0a1ab79e107b" - }, - { - "id": "7049b64d-8c7a-4341-b57e-556b28851c07", - "created_date": "2025-01-20 16:04:41.019000", - "last_modified_date": "2025-01-21 12:07:05.439000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "Version", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" - }, - { - "id": "7267cc8c-5aaa-4f40-a42a-c075a9aed8a6", - "created_date": "2025-01-20 16:04:41.082000", - "last_modified_date": "2025-01-21 12:07:05.563000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "c9060971-3f7d-4eb1-89e9-bb40d8687e77" - }, - { - "id": "7404eb62-541f-4a56-8040-1a99b6278a0c", - "created_date": "2025-01-20 16:04:41.078000", - "last_modified_date": "2025-01-21 12:07:05.554000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "c9060971-3f7d-4eb1-89e9-bb40d8687e77" - }, - { - "id": "74afd75d-90d0-47de-9d7a-63d64dde3119", - "created_date": "2025-01-20 16:04:41.617000", - "last_modified_date": "2025-01-21 12:07:06.446000", - "version": 1, - "column_name": "column_type", - "column_sync_name": "column_type", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 7, - "is_shown": 1, - "column_label": "Type", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" - }, - { - "id": "75c2550b-68da-41f6-a301-19c4f199750b", - "created_date": "2025-01-20 16:04:41.125000", - "last_modified_date": "2025-01-21 12:07:05.630000", - "version": 1, - "column_name": "title", - "column_sync_name": "title", - "column_type": "TEXT", - "column_modifier": "UNIQUE", - "column_order": 7, - "is_shown": 1, - "column_label": "Title", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "26478de9-0bcc-427f-ba60-0a1ab79e107b" - }, - { - "id": "785fb5fd-41b0-41a5-8bcf-89162d0abaf7", - "created_date": "2025-01-20 16:04:41.516000", - "last_modified_date": "2025-01-21 12:07:06.260000", - "version": 1, - "column_name": "last_used_date", - "column_sync_name": "used", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 7, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "d8b65435-dacc-4129-b08a-f4588110e127" - }, - { - "id": "7bcc1735-5eb8-4b2a-8921-12918dbbb793", - "created_date": "2025-01-20 16:04:41.582000", - "last_modified_date": "2025-01-21 12:07:06.374000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "f99eb13a-b7c6-4f51-940e-2a2c36124a67" - }, - { - "id": "7bde6aed-6d33-428d-8881-fab387298852", - "created_date": "2025-01-20 16:04:41.497000", - "last_modified_date": "2025-01-21 12:07:06.226000", - "version": 1, - "column_name": "email", - "column_sync_name": "email", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 8, - "is_shown": 1, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c" - }, - { - "id": "7ca4ea1b-1ed6-4f1a-a55d-9ea47b467678", - "created_date": "2025-01-20 16:04:41.475000", - "last_modified_date": "2025-01-21 12:07:06.182000", - "version": 1, - "column_name": "card_number", - "column_sync_name": "card_number", - "column_type": "LONG", - "column_modifier": null, - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "727a04ab-67dd-4a80-b80c-d824e41bcda2" - }, - { - "id": "7decae7f-fbf1-4cbb-8ed4-907bbc582141", - "created_date": "2025-01-20 16:04:41.425000", - "last_modified_date": "2025-01-21 12:07:06.083000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "9a1a6de3-eae1-48e7-88c4-0c67908ec1bf" - }, - { - "id": "7df0e865-9888-41b4-a407-c7d5d88a6965", - "created_date": "2025-01-20 16:04:41.095000", - "last_modified_date": "2025-01-21 12:07:05.583000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "33b748ee-9f55-487a-a614-2ab64b844a13" - }, - { - "id": "7e30dfe6-6a6a-48da-8c15-d6464f77cdfe", - "created_date": "2025-01-20 16:04:41.284000", - "last_modified_date": "2025-01-21 12:07:05.879000", - "version": 1, - "column_name": "title", - "column_sync_name": "title", - "column_type": "TEXT", - "column_modifier": "UNIQUE", - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "721a49ce-a02d-4035-add9-126a4591cbb8" - }, - { - "id": "7ea438b6-eb65-4c1a-a270-cbd3e6e3f8d9", - "created_date": "2025-01-20 16:04:41.429000", - "last_modified_date": "2025-01-21 12:07:06.089000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "9a1a6de3-eae1-48e7-88c4-0c67908ec1bf" - }, - { - "id": "7f1861b0-f1b2-4c28-8f5a-d43e8411afd8", - "created_date": "2025-01-20 16:04:41.496000", - "last_modified_date": "2025-01-21 12:07:06.223000", - "version": 1, - "column_name": "user_name", - "column_sync_name": "user_name", - "column_type": "TEXT", - "column_modifier": "UNIQUE", - "column_order": 7, - "is_shown": 1, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c" - }, - { - "id": "7f39bac1-7d9c-42b2-9b54-701242220258", - "created_date": "2025-01-20 16:04:40.994000", - "last_modified_date": "2025-01-21 12:07:05.379000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "63285ce5-eb45-47f5-9575-dec1f9778bbc" - }, - { - "id": "7f54d6da-9548-49d5-b064-8a5c47ac69b9", - "created_date": "2025-01-20 16:04:41.480000", - "last_modified_date": "2025-01-21 12:07:06.197000", - "version": 1, - "column_name": "vendor_id", - "column_sync_name": "vendor_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 9, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "727a04ab-67dd-4a80-b80c-d824e41bcda2" - }, - { - "id": "80a0742a-52d1-4014-808f-85eace053174", - "created_date": "2025-01-20 16:04:41.381000", - "last_modified_date": "2025-01-21 12:07:06.014000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "cf9b16b9-1ec0-4bed-ba95-fef65013c069" - }, - { - "id": "80de3ee1-1838-41c1-af2a-2f62d7312051", - "created_date": "2025-01-20 16:04:41.203000", - "last_modified_date": "2025-01-21 12:07:05.751000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "e4ab0e7b-4017-4ec0-9b33-b182a1850fda" - }, - { - "id": "82a99e30-a35c-4bba-b535-f2ebb9c19ca4", - "created_date": "2025-01-20 16:04:41.574000", - "last_modified_date": "2025-01-21 12:07:06.361000", - "version": 1, - "column_name": "user_name", - "column_sync_name": "user_name", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 8, - "is_shown": 1, - "column_label": "Username", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "d7584a1a-0249-45ed-85e9-532680643296" - }, - { - "id": "8495a964-57f1-4047-a95f-b8a27ba723c0", - "created_date": "2025-01-20 16:04:41.444000", - "last_modified_date": "2025-01-21 12:07:06.112000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "3ce78ef2-35c6-4397-8cdf-9241b52236df" - }, - { - "id": "853836ab-36ba-41d3-93f5-a85d56f95c76", - "created_date": "2025-01-20 16:04:41.186000", - "last_modified_date": "2025-01-21 12:07:05.723000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "ede5536c-ca16-48ec-bd0d-a3b00cc1fcab" - }, - { - "id": "85575a50-9a98-4a30-be18-300ed6325c93", - "created_date": "2025-01-20 16:04:41.600000", - "last_modified_date": "2025-01-21 12:07:06.411000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "058fa05e-2dab-4f61-8415-b5beec1fe79e" - }, - { - "id": "8591dd8b-21fd-45d1-8307-0299bee2858f", - "created_date": "2025-01-20 16:04:41.395000", - "last_modified_date": "2025-01-21 12:07:06.035000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "a9170107-b28f-4476-947e-0aaa3a2a1e01" - }, - { - "id": "8f8d7d86-894b-4806-8faf-9405a4e30c3f", - "created_date": "2025-01-20 16:04:41.457000", - "last_modified_date": "2025-01-21 12:07:06.139000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "a7a24f23-586b-4ac4-97ab-b610e2ab2e5d" - }, - { - "id": "90b0e104-cad3-4dd9-8bb2-ddb8abfef922", - "created_date": "2025-01-20 16:04:41.351000", - "last_modified_date": "2025-01-21 12:07:05.969000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "c96112de-b521-4e92-8021-0172a1aebaf8" - }, - { - "id": "9660d331-cbbf-4f4e-97c0-a6ba6160de6c", - "created_date": "2025-01-20 16:04:41.061000", - "last_modified_date": "2025-01-21 12:07:05.520000", - "version": 1, - "column_name": "review", - "column_sync_name": "review", - "column_type": "BOOLEAN", - "column_modifier": null, - "column_order": 6, - "is_shown": 1, - "column_label": "Review", - "show_filter": 1, - "filter_label": "Review", - "ref_column": null, - "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" - }, - { - "id": "9726256c-8951-4e7b-b6a3-2fe2722ef241", - "created_date": "2025-01-20 16:04:41.459000", - "last_modified_date": "2025-01-21 12:07:06.146000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "a7a24f23-586b-4ac4-97ab-b610e2ab2e5d" - }, - { - "id": "9769122a-ea9c-4b82-861b-2e0b7c701a11", - "created_date": "2025-01-20 16:04:41.155000", - "last_modified_date": "2025-01-21 12:07:05.674000", - "version": 1, - "column_name": "comic_id", - "column_sync_name": "comic_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 8, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": "title", - "table_id": "e86c5386-a155-4f40-a6f5-e8da635d39c4" - }, - { - "id": "9aa5d3a3-5af8-4fd4-a279-fef80d04ca8e", - "created_date": "2025-01-20 16:04:41.464000", - "last_modified_date": "2025-01-21 12:07:06.157000", - "version": 1, - "column_name": "name", - "column_sync_name": "name", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 7, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "a7a24f23-586b-4ac4-97ab-b610e2ab2e5d" - }, - { - "id": "9b494fb5-9d2f-43d2-91f2-7e6378fcd920", - "created_date": "2025-01-20 16:04:41.479000", - "last_modified_date": "2025-01-21 12:07:06.192000", - "version": 1, - "column_name": "rooster_id", - "column_sync_name": "rooster_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 8, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "727a04ab-67dd-4a80-b80c-d824e41bcda2" - }, - { - "id": "9cf2a827-633b-4506-889e-07be2c42c135", - "created_date": "2025-01-20 16:04:41.049000", - "last_modified_date": "2025-01-21 12:07:05.493000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 1, - "column_label": "ID", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" - }, - { - "id": "9d4b550e-f91e-4952-9803-f44055827ca3", - "created_date": "2025-01-20 16:04:41.168000", - "last_modified_date": "2025-01-21 12:07:05.692000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "13ea1a84-f92e-4ce6-b709-fe7c1893d229" - }, - { - "id": "9e2b8f30-e531-4911-9d4f-a66e4cae0104", - "created_date": "2025-01-20 16:04:41.184000", - "last_modified_date": "2025-01-21 12:07:05.719000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "ede5536c-ca16-48ec-bd0d-a3b00cc1fcab" - }, - { - "id": "9e674c4a-c227-471e-b088-ef146f7bbfe4", - "created_date": "2025-01-20 16:04:41.345000", - "last_modified_date": "2025-01-21 12:07:05.963000", - "version": 1, - "column_name": "book_id", - "column_sync_name": "book_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 6, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "e962f787-7795-4179-8168-b2992981da12" - }, - { - "id": "9e7e8cc9-b8f2-4078-9b4b-eb2241c333df", - "created_date": "2025-01-20 16:04:41.136000", - "last_modified_date": "2025-01-21 12:07:05.642000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "e86c5386-a155-4f40-a6f5-e8da635d39c4" - }, - { - "id": "9f5d1b59-658e-4cc8-8296-054f3f8d2b36", - "created_date": "2025-01-20 16:04:41.446000", - "last_modified_date": "2025-01-21 12:07:06.116000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "3ce78ef2-35c6-4397-8cdf-9241b52236df" - }, - { - "id": "9f62bb33-60dd-45ef-b911-9d5de31c9115", - "created_date": "2025-01-20 16:04:41.505000", - "last_modified_date": "2025-01-21 12:07:06.241000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "d8b65435-dacc-4129-b08a-f4588110e127" - }, - { - "id": "a078d8d6-61d9-4f5f-8f3c-7e3003772b31", - "created_date": "2025-01-20 16:04:41.630000", - "last_modified_date": "2025-01-21 12:07:06.472000", - "version": 1, - "column_name": "table_id", - "column_sync_name": "table_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 15, - "is_shown": 1, - "column_label": "Table", - "show_filter": 0, - "filter_label": null, - "ref_column": "table_name", - "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" - }, - { - "id": "a1006598-c0a4-49b6-b964-2c7d803149b6", - "created_date": "2025-01-20 16:04:41.336000", - "last_modified_date": "2025-01-21 12:07:05.951000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "e962f787-7795-4179-8168-b2992981da12" - }, - { - "id": "a2f3ea1d-2273-411a-bda9-e784a675987b", - "created_date": "2025-01-20 16:04:41.032000", - "last_modified_date": "2025-01-21 12:07:05.465000", - "version": 1, - "column_name": "title", - "column_sync_name": "title", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 8, - "is_shown": 1, - "column_label": "Title", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" - }, - { - "id": "a351720e-2bff-41a4-afd9-07a8f0409738", - "created_date": "2025-01-20 16:04:41.555000", - "last_modified_date": "2025-01-21 12:07:06.324000", - "version": 1, - "column_name": "module_name", - "column_sync_name": "module_name", - "column_type": "TEXT", - "column_modifier": "UNIQUE", - "column_order": 5, - "is_shown": 1, - "column_label": "Module", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "c92e35fc-b634-437a-bfa0-47d15c67fe83" - }, - { - "id": "a382ff07-84ac-4a1c-9fc1-4991b03fa117", - "created_date": "2025-01-20 16:04:41.566000", - "last_modified_date": "2025-01-21 12:07:06.340000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "d7584a1a-0249-45ed-85e9-532680643296" - }, - { - "id": "a507bbd8-c135-47f5-9294-4aee66335189", - "created_date": "2025-01-20 16:04:41.414000", - "last_modified_date": "2025-01-21 12:07:06.067000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "3773ac76-a957-42b2-b24d-5e66e4c32c12" - }, - { - "id": "a5358b3f-35cf-4268-b84d-6122adb4da84", - "created_date": "2025-01-20 16:04:41.017000", - "last_modified_date": "2025-01-21 12:07:05.432000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" - }, - { - "id": "a5711833-d162-4dd8-a2eb-694b3f70cae2", - "created_date": "2025-01-20 16:04:41.300000", - "last_modified_date": "2025-01-21 12:07:05.907000", - "version": 1, - "column_name": "author_id", - "column_sync_name": "author_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 6, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "7ebdc79a-d0a7-42dc-9e50-d93c79e182af" - }, - { - "id": "a756ca5d-b2a2-4d5e-8445-bac51633f340", - "created_date": "2025-01-20 16:04:41.551000", - "last_modified_date": "2025-01-21 12:07:06.314000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "c92e35fc-b634-437a-bfa0-47d15c67fe83" - }, - { - "id": "a7c70442-e850-4fc1-a3d6-330c2054f1d2", - "created_date": "2025-01-20 16:04:41.357000", - "last_modified_date": "2025-01-21 12:07:05.979000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "c96112de-b521-4e92-8021-0172a1aebaf8" - }, - { - "id": "a95be86c-310c-454a-ac15-0175f01bcc73", - "created_date": "2025-01-20 16:04:41.529000", - "last_modified_date": "2025-01-21 12:07:06.276000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "925989d3-faa9-4bb4-9183-3564b29063ec" - }, - { - "id": "a96e736e-4881-4a12-ac3f-4b24097cddde", - "created_date": "2025-01-20 16:04:41.619000", - "last_modified_date": "2025-01-21 12:07:06.449000", - "version": 1, - "column_name": "column_modifier", - "column_sync_name": "column_modifier", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 8, - "is_shown": 1, - "column_label": "Modifier", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" - }, - { - "id": "aaa29939-121e-4f0b-bacc-ac18c5c0288d", - "created_date": "2025-01-20 16:04:41.070000", - "last_modified_date": "2025-01-21 12:07:05.540000", - "version": 1, - "column_name": "path", - "column_sync_name": "path", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 10, - "is_shown": 1, - "column_label": "Verzeichnis", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" - }, - { - "id": "aff31eb7-2ec6-47ca-aab6-db72c7e44f35", - "created_date": "2025-01-20 16:04:41.399000", - "last_modified_date": "2025-01-21 12:07:06.041000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "a9170107-b28f-4476-947e-0aaa3a2a1e01" - }, - { - "id": "b096272e-a31e-41de-bd14-55f5179ee4f1", - "created_date": "2025-01-20 16:04:41.461000", - "last_modified_date": "2025-01-21 12:07:06.150000", - "version": 1, - "column_name": "insert_set", - "column_sync_name": "insert_set", - "column_type": "BOOLEAN", - "column_modifier": null, - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "a7a24f23-586b-4ac4-97ab-b610e2ab2e5d" - }, - { - "id": "b268026e-f505-4eb3-b729-dcf2a62dd481", - "created_date": "2025-01-20 16:04:41.146000", - "last_modified_date": "2025-01-21 12:07:05.656000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "e86c5386-a155-4f40-a6f5-e8da635d39c4" - }, - { - "id": "b2fd1e48-3b1b-4304-8861-f076f32fda54", - "created_date": "2025-01-20 16:04:41.038000", - "last_modified_date": "2025-01-21 12:07:05.478000", - "version": 1, - "column_name": "path", - "column_sync_name": "path", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 10, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" - }, - { - "id": "b4d20305-8b61-4a43-95a7-e6b1e0441951", - "created_date": "2025-01-20 16:04:41.418000", - "last_modified_date": "2025-01-21 12:07:06.073000", - "version": 1, - "column_name": "name", - "column_sync_name": "name", - "column_type": "TEXT", - "column_modifier": "UNIQUE", - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "3773ac76-a957-42b2-b24d-5e66e4c32c12" - }, - { - "id": "b5bc90fc-5c08-4861-9613-e76234ce00e4", - "created_date": "2025-01-20 16:04:41.514000", - "last_modified_date": "2025-01-21 12:07:06.256000", - "version": 1, - "column_name": "name", - "column_sync_name": "name", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 6, - "is_shown": 1, - "column_label": "Name", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "d8b65435-dacc-4129-b08a-f4588110e127" - }, - { - "id": "b6185d30-db67-418a-923e-52097c7575b3", - "created_date": "2025-01-20 16:04:41.411000", - "last_modified_date": "2025-01-21 12:07:06.059000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "3773ac76-a957-42b2-b24d-5e66e4c32c12" - }, - { - "id": "b6834a12-ba8a-48c0-b28b-7975a8844f0b", - "created_date": "2025-01-20 16:04:41.379000", - "last_modified_date": "2025-01-21 12:07:06.010000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "cf9b16b9-1ec0-4bed-ba95-fef65013c069" - }, - { - "id": "b7522c46-21eb-4811-a407-60fc413064af", - "created_date": "2025-01-20 16:04:41.295000", - "last_modified_date": "2025-01-21 12:07:05.899000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "7ebdc79a-d0a7-42dc-9e50-d93c79e182af" - }, - { - "id": "b97c0905-ab37-4ef8-9cc9-128c4352eb70", - "created_date": "2025-01-20 16:04:41.587000", - "last_modified_date": "2025-01-21 12:07:06.384000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "f99eb13a-b7c6-4f51-940e-2a2c36124a67" - }, - { - "id": "ba27d149-3c4e-4861-9482-426fa068be6c", - "created_date": "2025-01-20 16:04:41.148000", - "last_modified_date": "2025-01-21 12:07:05.660000", - "version": 1, - "column_name": "in_stock", - "column_sync_name": "in_stock", - "column_type": "BOOLEAN", - "column_modifier": null, - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "e86c5386-a155-4f40-a6f5-e8da635d39c4" - }, - { - "id": "bb720b08-b328-40bf-a93a-480f5d44dec8", - "created_date": "2025-01-20 16:04:41.397000", - "last_modified_date": "2025-01-21 12:07:06.038000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "a9170107-b28f-4476-947e-0aaa3a2a1e01" - }, - { - "id": "bb8ed0d2-da5f-4adf-9bdc-763af6b867b5", - "created_date": "2025-01-20 16:04:41.172000", - "last_modified_date": "2025-01-21 12:07:05.700000", - "version": 1, - "column_name": "name", - "column_sync_name": "name", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "13ea1a84-f92e-4ce6-b709-fe7c1893d229" - }, - { - "id": "bbeb1f07-ee1e-44b5-a6af-755e5090a64b", - "created_date": "2025-01-20 16:04:41.509000", - "last_modified_date": "2025-01-21 12:07:06.249000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "d8b65435-dacc-4129-b08a-f4588110e127" - }, - { - "id": "bc92dd14-8330-4c42-9a14-80edd8a84837", - "created_date": "2025-01-20 16:04:41.059000", - "last_modified_date": "2025-01-21 12:07:05.515000", - "version": 1, - "column_name": "url", - "column_sync_name": "link_url", - "column_type": "TEXT", - "column_modifier": "UNIQUE", - "column_order": 5, - "is_shown": 1, - "column_label": "URL", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" - }, - { - "id": "bd28b532-2354-4539-8041-fe4420f12066", - "created_date": "2025-01-20 16:04:41.570000", - "last_modified_date": "2025-01-21 12:07:06.349000", - "version": 1, - "column_name": "host", - "column_sync_name": "host", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 5, - "is_shown": 1, - "column_label": "Host", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "d7584a1a-0249-45ed-85e9-532680643296" - }, - { - "id": "bd3a6721-7d23-40e3-8e98-234e1f5edd8f", - "created_date": "2025-01-20 16:04:41.449000", - "last_modified_date": "2025-01-21 12:07:06.123000", - "version": 1, - "column_name": "player_id", - "column_sync_name": "player_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 6, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "3ce78ef2-35c6-4397-8cdf-9241b52236df" - }, - { - "id": "be352526-a240-4b0f-b9c1-87949d6a631e", - "created_date": "2025-01-20 16:04:41.193000", - "last_modified_date": "2025-01-21 12:07:05.736000", - "version": 1, - "column_name": "name", - "column_sync_name": "name", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 7, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "ede5536c-ca16-48ec-bd0d-a3b00cc1fcab" - }, - { - "id": "beee0fd2-ee99-4295-a66d-2b79abc0d266", - "created_date": "2025-01-20 16:04:41.248000", - "last_modified_date": "2025-01-21 12:07:05.820000", - "version": 1, - "column_name": "artist_id", - "column_sync_name": "artist_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "ae68f37b-9314-472c-86ea-387486f4db1c" - }, - { - "id": "befb9c7e-5075-4d76-9553-d2c91907ca5d", - "created_date": "2025-01-20 16:04:41.552000", - "last_modified_date": "2025-01-21 12:07:06.317000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "c92e35fc-b634-437a-bfa0-47d15c67fe83" - }, - { - "id": "c0d3f441-6227-446f-92f2-58d7c36da5e7", - "created_date": "2025-01-20 16:04:41.451000", - "last_modified_date": "2025-01-21 12:07:06.131000", - "version": 1, - "column_name": "team_id", - "column_sync_name": "team_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 8, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "3ce78ef2-35c6-4397-8cdf-9241b52236df" - }, - { - "id": "c11afaf1-5d18-4ba3-b9ff-bec4a5493af7", - "created_date": "2025-01-20 16:04:41.447000", - "last_modified_date": "2025-01-21 12:07:06.119000", - "version": 1, - "column_name": "year", - "column_sync_name": "year", - "column_type": "LONG", - "column_modifier": null, - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "3ce78ef2-35c6-4397-8cdf-9241b52236df" - }, - { - "id": "c13a35ae-b86f-4d79-a752-fb24c74b9f77", - "created_date": "2025-01-20 16:04:41.543000", - "last_modified_date": "2025-01-21 12:07:06.298000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "36c7ac9c-270d-40a4-af43-c952120f165c" - }, - { - "id": "c1989ee0-b0d0-4e01-983b-312276a1d328", - "created_date": "2025-01-20 16:04:41.276000", - "last_modified_date": "2025-01-21 12:07:05.868000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "721a49ce-a02d-4035-add9-126a4591cbb8" - }, - { - "id": "c2797d33-e8b9-4799-9025-0846b91f5527", - "created_date": "2025-01-20 16:04:41.629000", - "last_modified_date": "2025-01-21 12:07:06.468000", - "version": 1, - "column_name": "ref_column", - "column_sync_name": "ref_column", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 14, - "is_shown": 1, - "column_label": "Ref Column", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" - }, - { - "id": "c3058c53-38ee-4ea3-b9a4-7615e7a9448c", - "created_date": "2025-01-20 16:04:41.613000", - "last_modified_date": "2025-01-21 12:07:06.436000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" - }, - { - "id": "c37d106f-fe44-4e26-86e1-eeb433eb90e8", - "created_date": "2025-01-20 16:04:41.383000", - "last_modified_date": "2025-01-21 12:07:06.018000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "cf9b16b9-1ec0-4bed-ba95-fef65013c069" - }, - { - "id": "c6a6043e-f124-4762-913f-e25b81ef1572", - "created_date": "2025-01-20 16:04:41.493000", - "last_modified_date": "2025-01-21 12:07:06.216000", - "version": 1, - "column_name": "first_name", - "column_sync_name": "first_name", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 5, - "is_shown": 1, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c" - }, - { - "id": "c87d13f4-e745-4dfa-9a36-91c7715fa47a", - "created_date": "2025-01-20 16:04:41.263000", - "last_modified_date": "2025-01-21 12:07:05.848000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "1951f2b6-3640-4568-9c47-a20e0768a843" - }, - { - "id": "c8fb8955-7b61-4e52-b045-02035408fbd8", - "created_date": "2025-01-20 16:04:41.557000", - "last_modified_date": "2025-01-21 12:07:06.327000", - "version": 1, - "column_name": "import_data", - "column_sync_name": "import_data", - "column_type": "BOOLEAN", - "column_modifier": null, - "column_order": 6, - "is_shown": 1, - "column_label": "Import Data?", - "show_filter": 1, - "filter_label": "Import Data", - "ref_column": null, - "table_id": "c92e35fc-b634-437a-bfa0-47d15c67fe83" - }, - { - "id": "c9a7e0eb-412c-4bd1-a59a-05a0f6f14cdb", - "created_date": "2025-01-20 16:04:41.157000", - "last_modified_date": "2025-01-21 12:07:05.678000", - "version": 1, - "column_name": "volume_id", - "column_sync_name": "volume_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 9, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "e86c5386-a155-4f40-a6f5-e8da635d39c4" - }, - { - "id": "cb46dc70-6d43-4c3e-92b6-9686077a15aa", - "created_date": "2025-01-20 16:04:41.128000", - "last_modified_date": "2025-01-21 12:07:05.635000", - "version": 1, - "column_name": "publisher_id", - "column_sync_name": "publisher_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 8, - "is_shown": 1, - "column_label": "Verlag", - "show_filter": 0, - "filter_label": null, - "ref_column": "name", - "table_id": "26478de9-0bcc-427f-ba60-0a1ab79e107b" - }, - { - "id": "cc345232-345d-40ca-9440-81f6142ff0f9", - "created_date": "2025-01-20 16:04:41.571000", - "last_modified_date": "2025-01-21 12:07:06.353000", - "version": 1, - "column_name": "port", - "column_sync_name": "port", - "column_type": "LONG", - "column_modifier": null, - "column_order": 6, - "is_shown": 1, - "column_label": "Port", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "d7584a1a-0249-45ed-85e9-532680643296" - }, - { - "id": "ceac312b-fcc6-4fd1-a9d7-6ca8d86e9c02", - "created_date": "2025-01-20 16:04:41.210000", - "last_modified_date": "2025-01-21 12:07:05.762000", - "version": 1, - "column_name": "name", - "column_sync_name": "name", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "e4ab0e7b-4017-4ec0-9b33-b182a1850fda" - }, - { - "id": "cff3f14e-2af8-4267-9490-0b33d96da350", - "created_date": "2025-01-20 16:04:41.293000", - "last_modified_date": "2025-01-21 12:07:05.895000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "7ebdc79a-d0a7-42dc-9e50-d93c79e182af" - }, - { - "id": "d03c18ad-f7df-419c-8de9-a29540a607a6", - "created_date": "2025-01-20 16:04:41.014000", - "last_modified_date": "2025-01-21 12:07:05.426000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" - }, - { - "id": "d05c7280-9e5c-4afe-aa18-ce0b065731c3", - "created_date": "2025-01-20 16:04:41.612000", - "last_modified_date": "2025-01-21 12:07:06.432000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" - }, - { - "id": "d19e8269-31c6-4665-a6c8-aff4b2f687c2", - "created_date": "2025-01-20 16:04:41.494000", - "last_modified_date": "2025-01-21 12:07:06.219000", - "version": 1, - "column_name": "last_name", - "column_sync_name": "last_name", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 6, - "is_shown": 1, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c" - }, - { - "id": "d1dace0e-34fc-4b38-b5aa-0baf83a81f90", - "created_date": "2025-01-20 16:04:41.292000", - "last_modified_date": "2025-01-21 12:07:05.891000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "7ebdc79a-d0a7-42dc-9e50-d93c79e182af" - }, - { - "id": "d2220d74-e41c-4198-8ae1-535fd0d6cccb", - "created_date": "2025-01-20 16:04:41.621000", - "last_modified_date": "2025-01-21 12:07:06.452000", - "version": 1, - "column_name": "column_order", - "column_sync_name": "column_order", - "column_type": "LONG", - "column_modifier": null, - "column_order": 9, - "is_shown": 1, - "column_label": "Order", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" - }, - { - "id": "d2273139-9e75-44d2-b90b-a65281ab4d90", - "created_date": "2025-01-20 16:04:41.208000", - "last_modified_date": "2025-01-21 12:07:05.759000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "e4ab0e7b-4017-4ec0-9b33-b182a1850fda" - }, - { - "id": "d2388abd-b2fc-41ed-96af-93219db104c0", - "created_date": "2025-01-20 16:04:41.504000", - "last_modified_date": "2025-01-21 12:07:06.238000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 1, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "d8b65435-dacc-4129-b08a-f4588110e127" - }, - { - "id": "d2cbbac7-b7fa-4003-8fcd-106963b2f954", - "created_date": "2025-01-20 16:04:41.028000", - "last_modified_date": "2025-01-21 12:07:05.460000", - "version": 1, - "column_name": "should_download", - "column_sync_name": "should_download", - "column_type": "BOOLEAN", - "column_modifier": null, - "column_order": 7, - "is_shown": 1, - "column_label": "Download", - "show_filter": 1, - "filter_label": "Download", - "ref_column": null, - "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" - }, - { - "id": "d4d7a891-edbc-404e-b022-5f1635217e8f", - "created_date": "2025-01-20 16:04:41.588000", - "last_modified_date": "2025-01-21 12:07:06.388000", - "version": 1, - "column_name": "folder", - "column_sync_name": "folder", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 5, - "is_shown": 1, - "column_label": "Folder", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "f99eb13a-b7c6-4f51-940e-2a2c36124a67" - }, - { - "id": "d5d5a476-4497-49ae-8293-664f0b1ba8c2", - "created_date": "2025-01-20 16:04:41.119000", - "last_modified_date": "2025-01-21 12:07:05.621000", - "version": 1, - "column_name": "completed", - "column_sync_name": "completed", - "column_type": "BOOLEAN", - "column_modifier": null, - "column_order": 5, - "is_shown": 1, - "column_label": "Complete", - "show_filter": 1, - "filter_label": "Complete", - "ref_column": null, - "table_id": "26478de9-0bcc-427f-ba60-0a1ab79e107b" - }, - { - "id": "d5eff9c6-dfde-475a-949c-85374de3e846", - "created_date": "2025-01-20 16:04:41.359000", - "last_modified_date": "2025-01-21 12:07:05.983000", - "version": 1, - "column_name": "name", - "column_sync_name": "name", - "column_type": "TEXT", - "column_modifier": "UNIQUE", - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "c96112de-b521-4e92-8021-0172a1aebaf8" - }, - { - "id": "d6ab11fa-63ff-4ad2-9c49-c6b95c50de4a", - "created_date": "2025-01-20 16:04:41.370000", - "last_modified_date": "2025-01-21 12:07:06", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "1850a718-dd07-4766-9856-591d311a4d0d" - }, - { - "id": "d7f6b9ff-df50-42b6-90e9-c4dbf0f692eb", - "created_date": "2025-01-20 16:04:41.622000", - "last_modified_date": "2025-01-21 12:07:06.455000", - "version": 1, - "column_name": "is_shown", - "column_sync_name": "is_shown", - "column_type": "BOOLEAN", - "column_modifier": null, - "column_order": 10, - "is_shown": 1, - "column_label": "Is Shown", - "show_filter": 1, - "filter_label": "Is Shown", - "ref_column": null, - "table_id": "245402a4-ee6a-404b-afb5-6a95ed88469f" - }, - { - "id": "dc6ddb0e-98ed-4c69-9072-2c8e96e04a55", - "created_date": "2025-01-20 16:04:41.290000", - "last_modified_date": "2025-01-21 12:07:05.886000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "7ebdc79a-d0a7-42dc-9e50-d93c79e182af" - }, - { - "id": "dd194c0d-330c-4e88-948c-20d7e0f806a3", - "created_date": "2025-01-20 16:04:41.195000", - "last_modified_date": "2025-01-21 12:07:05.740000", - "version": 1, - "column_name": "comic_id", - "column_sync_name": "comic_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 8, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": "title", - "table_id": "ede5536c-ca16-48ec-bd0d-a3b00cc1fcab" - }, - { - "id": "dd7392d7-dc9b-4d84-acac-a6ceebd20324", - "created_date": "2025-01-20 16:04:41.486000", - "last_modified_date": "2025-01-21 12:07:06.206000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "65a10e0a-5833-410d-b0ff-664eed4c1a5c" - }, - { - "id": "dfdeabdf-5820-4d05-a790-353ad1eda8e4", - "created_date": "2025-01-20 16:04:41.212000", - "last_modified_date": "2025-01-21 12:07:05.766000", - "version": 1, - "column_name": "comic_id", - "column_sync_name": "comic_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 6, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "e4ab0e7b-4017-4ec0-9b33-b182a1850fda" - }, - { - "id": "e074a44e-79c7-464b-8959-1e971a27fe9f", - "created_date": "2025-01-20 16:04:41.259000", - "last_modified_date": "2025-01-21 12:07:05.839000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "1951f2b6-3640-4568-9c47-a20e0768a843" - }, - { - "id": "e1ca67f3-94d3-4459-91aa-f487087e83d6", - "created_date": "2025-01-20 16:04:41.063000", - "last_modified_date": "2025-01-21 12:07:05.526000", - "version": 1, - "column_name": "should_download", - "column_sync_name": "should_download", - "column_type": "BOOLEAN", - "column_modifier": null, - "column_order": 7, - "is_shown": 1, - "column_label": "Download", - "show_filter": 1, - "filter_label": "Download", - "ref_column": null, - "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" - }, - { - "id": "e2a22f91-7fb9-4261-a2a2-63a39141f5af", - "created_date": "2025-01-20 16:04:41.390000", - "last_modified_date": "2025-01-21 12:07:06.029000", - "version": 1, - "column_name": "last_name", - "column_sync_name": "last_name", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 6, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "cf9b16b9-1ec0-4bed-ba95-fef65013c069" - }, - { - "id": "e2d91173-a199-4e16-b190-4df3ed43e0f8", - "created_date": "2025-01-20 16:04:41.585000", - "last_modified_date": "2025-01-21 12:07:06.381000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "f99eb13a-b7c6-4f51-940e-2a2c36124a67" - }, - { - "id": "e2dd6cd2-188c-4423-a1f5-ab4c7ce421ab", - "created_date": "2025-01-20 16:04:41.513000", - "last_modified_date": "2025-01-21 12:07:06.253000", - "version": 1, - "column_name": "token", - "column_sync_name": "token", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 5, - "is_shown": 1, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "d8b65435-dacc-4129-b08a-f4588110e127" - }, - { - "id": "e45a35b2-9949-429c-967c-7ed3022ab115", - "created_date": "2025-01-20 16:04:41.450000", - "last_modified_date": "2025-01-21 12:07:06.127000", - "version": 1, - "column_name": "position_id", - "column_sync_name": "position_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 7, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "3ce78ef2-35c6-4397-8cdf-9241b52236df" - }, - { - "id": "e54bb4cb-748f-4b4a-b962-6b0b63d6cc59", - "created_date": "2025-01-20 16:04:41.087000", - "last_modified_date": "2025-01-21 12:07:05.573000", - "version": 1, - "column_name": "name", - "column_sync_name": "name", - "column_type": "TEXT", - "column_modifier": "UNIQUE", - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "c9060971-3f7d-4eb1-89e9-bb40d8687e77" - }, - { - "id": "e6d4e6e5-d859-4c6f-977a-43e77c5baff3", - "created_date": "2025-01-20 16:04:41.594000", - "last_modified_date": "2025-01-21 12:07:06.398000", - "version": 1, - "column_name": "sent_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 8, - "is_shown": 0, - "column_label": "Gesendet", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "f99eb13a-b7c6-4f51-940e-2a2c36124a67" - }, - { - "id": "e6e9e4cb-8a34-4ae6-aae3-961be9bd7ef1", - "created_date": "2025-01-20 16:04:41.544000", - "last_modified_date": "2025-01-21 12:07:06.302000", - "version": 1, - "column_name": "user_id", - "column_sync_name": "user_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 5, - "is_shown": 1, - "column_label": "User", - "show_filter": 0, - "filter_label": null, - "ref_column": "user_name", - "table_id": "36c7ac9c-270d-40a4-af43-c952120f165c" - }, - { - "id": "e7b24a7c-99f5-4101-937e-ffa1b9af115f", - "created_date": "2025-01-20 16:04:41.258000", - "last_modified_date": "2025-01-21 12:07:05.835000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "1951f2b6-3640-4568-9c47-a20e0768a843" - }, - { - "id": "e7cb1343-2a42-4a23-a894-5dd16e218afd", - "created_date": "2025-01-20 16:04:41.470000", - "last_modified_date": "2025-01-21 12:07:06.167000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "727a04ab-67dd-4a80-b80c-d824e41bcda2" - }, - { - "id": "e836258d-6315-40ee-97d3-33bec04d87fd", - "created_date": "2025-01-20 16:04:41.576000", - "last_modified_date": "2025-01-21 12:07:06.365000", - "version": 1, - "column_name": "password", - "column_sync_name": "password", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 9, - "is_shown": 0, - "column_label": "Password", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "d7584a1a-0249-45ed-85e9-532680643296" - }, - { - "id": "e8ee73c8-0b98-44a7-ac19-5b39a9eb0419", - "created_date": "2025-01-20 16:04:41.431000", - "last_modified_date": "2025-01-21 12:07:06.092000", - "version": 1, - "column_name": "name", - "column_sync_name": "name", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "9a1a6de3-eae1-48e7-88c4-0c67908ec1bf" - }, - { - "id": "ea50b9fc-6fab-4ee1-bb99-c64fa0ea781c", - "created_date": "2025-01-20 16:04:41.281000", - "last_modified_date": "2025-01-21 12:07:05.876000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "721a49ce-a02d-4035-add9-126a4591cbb8" - }, - { - "id": "eba35146-c5bf-41b8-b094-bb069413af8f", - "created_date": "2025-01-20 16:04:41.205000", - "last_modified_date": "2025-01-21 12:07:05.755000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "e4ab0e7b-4017-4ec0-9b33-b182a1850fda" - }, - { - "id": "eee39698-9f5c-4e89-a7d5-3b528eb9f185", - "created_date": "2025-01-20 16:04:41.562000", - "last_modified_date": "2025-01-21 12:07:06.333000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "d7584a1a-0249-45ed-85e9-532680643296" - }, - { - "id": "ef650efc-ba09-4ddd-882a-df0ff6ab745a", - "created_date": "2025-01-20 16:04:41.003000", - "last_modified_date": "2025-01-21 12:07:05.405000", - "version": 1, - "column_name": "review", - "column_sync_name": "review", - "column_type": "BOOLEAN", - "column_modifier": null, - "column_order": 6, - "is_shown": 1, - "column_label": "Review", - "show_filter": 1, - "filter_label": "Review", - "ref_column": null, - "table_id": "63285ce5-eb45-47f5-9575-dec1f9778bbc" - }, - { - "id": "f136c838-f004-4acf-8a61-38a99411c323", - "created_date": "2025-01-20 16:04:41.541000", - "last_modified_date": "2025-01-21 12:07:06.294000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "36c7ac9c-270d-40a4-af43-c952120f165c" - }, - { - "id": "f1836283-a6d3-4bfa-88f4-3eaf4181381e", - "created_date": "2025-01-20 16:04:41.241000", - "last_modified_date": "2025-01-21 12:07:05.807000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "ae68f37b-9314-472c-86ea-387486f4db1c" - }, - { - "id": "f1afc822-563f-4560-9e2e-6f9e18b40b00", - "created_date": "2025-01-20 16:04:41.531000", - "last_modified_date": "2025-01-21 12:07:06.279000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "925989d3-faa9-4bb4-9183-3564b29063ec" - }, - { - "id": "f30edb57-49e5-4b33-9063-b90ec68be198", - "created_date": "2025-01-20 16:04:41.054000", - "last_modified_date": "2025-01-21 12:07:05.504000", - "version": 1, - "column_name": "last_modified_date", - "column_sync_name": "modified", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 3, - "is_shown": 0, - "column_label": "Modified", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "777015a8-f8b8-43b2-b285-8aae3885b7e7" - }, - { - "id": "f35111c8-09a3-45d6-aba7-ba7185cb2d41", - "created_date": "2025-01-20 16:04:41.100000", - "last_modified_date": "2025-01-21 12:07:05.591000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "33b748ee-9f55-487a-a614-2ab64b844a13" - }, - { - "id": "f614773e-52bf-4422-8ff7-5f1c6b4e46ab", - "created_date": "2025-01-20 16:04:41.572000", - "last_modified_date": "2025-01-21 12:07:06.357000", - "version": 1, - "column_name": "protocol", - "column_sync_name": "protocol", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 7, - "is_shown": 1, - "column_label": "Protocol", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "d7584a1a-0249-45ed-85e9-532680643296" - }, - { - "id": "f7bf5945-c316-4d93-a50d-636f64b6ecab", - "created_date": "2025-01-20 16:04:41.093000", - "last_modified_date": "2025-01-21 12:07:05.579000", - "version": 1, - "column_name": "id", - "column_sync_name": "identifier", - "column_type": "TEXT", - "column_modifier": "PRIMARY KEY", - "column_order": 1, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "33b748ee-9f55-487a-a614-2ab64b844a13" - }, - { - "id": "f82d7ffa-07ae-40d7-bd69-429dfcd28004", - "created_date": "2025-01-20 16:04:41.386000", - "last_modified_date": "2025-01-21 12:07:06.022000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "cf9b16b9-1ec0-4bed-ba95-fef65013c069" - }, - { - "id": "f83d93ca-9151-4d89-901e-d841a6ea5eba", - "created_date": "2025-01-20 16:04:41.111000", - "last_modified_date": "2025-01-21 12:07:05.606000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "26478de9-0bcc-427f-ba60-0a1ab79e107b" - }, - { - "id": "f8937b0c-73ee-4677-9011-e623b7415d0e", - "created_date": "2025-01-20 16:04:41.080000", - "last_modified_date": "2025-01-21 12:07:05.558000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "c9060971-3f7d-4eb1-89e9-bb40d8687e77" - }, - { - "id": "f9984d7f-b9c4-49e7-9277-7d8f295e21ae", - "created_date": "2025-01-20 16:04:41.035000", - "last_modified_date": "2025-01-21 12:07:05.472000", - "version": 1, - "column_name": "file_name", - "column_sync_name": "file_name", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 9, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "2f841ba9-48f3-4a44-876a-67044d43b5db" - }, - { - "id": "f9c63580-428d-4ccd-a02e-d40616dc9b48", - "created_date": "2025-01-20 16:04:41.265000", - "last_modified_date": "2025-01-21 12:07:05.852000", - "version": 1, - "column_name": "first_name", - "column_sync_name": "first_name", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 5, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "1951f2b6-3640-4568-9c47-a20e0768a843" - }, - { - "id": "fb661f6b-3693-4af8-8398-2e95278c538d", - "created_date": "2025-01-20 16:04:41.520000", - "last_modified_date": "2025-01-21 12:07:06.266000", - "version": 1, - "column_name": "user_id", - "column_sync_name": "user_id", - "column_type": "TEXT", - "column_modifier": null, - "column_order": 9, - "is_shown": 1, - "column_label": "User", - "show_filter": 0, - "filter_label": null, - "ref_column": "user_name", - "table_id": "d8b65435-dacc-4129-b08a-f4588110e127" - }, - { - "id": "fc565d02-e7bb-466e-9518-1a685b165419", - "created_date": "2025-01-20 16:04:41.181000", - "last_modified_date": "2025-01-21 12:07:05.714000", - "version": 1, - "column_name": "created_date", - "column_sync_name": "created", - "column_type": "TIMESTAMP", - "column_modifier": null, - "column_order": 2, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "ede5536c-ca16-48ec-bd0d-a3b00cc1fcab" - }, - { - "id": "fe55380f-938e-4b01-91a5-88a43795772f", - "created_date": "2025-01-20 16:04:41.416000", - "last_modified_date": "2025-01-21 12:07:06.070000", - "version": 1, - "column_name": "version", - "column_sync_name": "version", - "column_type": "LONG", - "column_modifier": null, - "column_order": 4, - "is_shown": 0, - "column_label": "", - "show_filter": 0, - "filter_label": null, - "ref_column": null, - "table_id": "3773ac76-a957-42b2-b24d-5e66e4c32c12" - } - ], - "comic": [ - { - "id": "0095a769-e5e2-441e-8a38-f98743ee9529", - "created_date": "2024-08-16 23:58:35.239000", - "last_modified_date": "2024-08-16 23:58:35.239000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Spider-Man: Breakout", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "010ffe6b-54e4-42d7-86b6-581e680c35ad", - "created_date": "2024-08-16 23:58:35.279000", - "last_modified_date": "2024-08-16 23:58:35.279000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "The Tenth", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "031b7570-04bc-4f56-834e-61c5792b6e5e", - "created_date": "2024-08-16 23:58:34.870000", - "last_modified_date": "2024-08-16 23:58:34.870000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Bart Simpson", - "publisher_id": "27b4304a-3bac-4ed3-9c2d-e8027d016dc0" - }, - { - "id": "03c5b145-69d4-4d7e-8323-cb2f81060829", - "created_date": "2024-08-16 23:58:34.903000", - "last_modified_date": "2024-08-16 23:58:34.903000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Danger Girl", - "publisher_id": "46cef536-96d3-442a-a871-fd133050053e" - }, - { - "id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", - "created_date": "2024-08-16 23:58:34.853000", - "last_modified_date": "2024-08-16 23:58:34.853000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Astonishing X-Men", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", - "created_date": "2024-08-16 23:58:35.161000", - "last_modified_date": "2024-08-16 23:58:35.161000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "New X-Men Academy X", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "07574acf-8f77-4da7-87fd-c49f40536baf", - "created_date": "2024-08-16 23:58:35.368000", - "last_modified_date": "2024-08-16 23:58:35.368000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Aria: The Uses of Enchantment", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "0cb7d5d0-4204-41a4-8698-b8dd56f1c597", - "created_date": "2024-08-16 23:58:35.308000", - "last_modified_date": "2024-08-16 23:58:35.308000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Vampirella", - "publisher_id": "c96d450d-5034-435b-9afe-c49c937b6328" - }, - { - "id": "0e6688d2-f347-4800-83b1-1f094b658084", - "created_date": "2024-08-16 23:58:35.198000", - "last_modified_date": "2024-08-16 23:58:35.198000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Shanna, The She-Devil", - "publisher_id": "978ef035-20af-4d89-b898-98159d8ce280" - }, - { - "id": "0f0bfd13-f6f0-436c-ad74-5e40ea6a0cf8", - "created_date": "2024-08-16 23:58:34.996000", - "last_modified_date": "2024-08-16 23:58:34.996000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Fathom: Killians Tide", - "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" - }, - { - "id": "0fd9969e-8e1f-4f42-94ac-fab4d2d614c2", - "created_date": "2024-08-16 23:58:34.849000", - "last_modified_date": "2024-08-16 23:58:34.849000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Aspen", - "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" - }, - { - "id": "100b82bf-c134-40d9-bcc2-a8b345030b8d", - "created_date": "2024-08-16 23:58:35.230000", - "last_modified_date": "2024-08-16 23:58:35.230000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Spider-Man India", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "11bc20d5-bfeb-4825-9e0f-3d6954020b07", - "created_date": "2024-08-16 23:58:34.943000", - "last_modified_date": "2024-08-16 23:58:34.943000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "El Cazador", - "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" - }, - { - "id": "12802b17-452a-4533-a7ab-fd94f19be123", - "created_date": "2024-08-16 23:58:34.977000", - "last_modified_date": "2024-08-16 23:58:34.977000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Fathom Dawn of War", - "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" - }, - { - "id": "12fde8f7-8ce3-4398-8095-5bb9b5d9508d", - "created_date": "2024-08-16 23:58:34.935000", - "last_modified_date": "2024-08-16 23:58:34.935000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Dragonlance: Chronicles", - "publisher_id": "1c3e0871-ca17-4766-935d-15e0ec33a5ac" - }, - { - "id": "13281ef7-5945-49a9-b8f1-c5e8548c18ec", - "created_date": "2024-08-16 23:58:35.331000", - "last_modified_date": "2024-08-16 23:58:35.331000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "X-23", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "134659ae-67a4-4cad-aac6-36bee893102f", - "created_date": "2024-08-16 23:58:35.354000", - "last_modified_date": "2024-08-16 23:58:35.354000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Runaways", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "14a359a2-c928-4e14-9fb2-249777e6a13a", - "created_date": "2024-08-16 23:58:35.364000", - "last_modified_date": "2024-08-16 23:58:35.364000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Aria: Summer\u00b4s Spell", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "17b50be7-aca7-446e-8729-3706f636d29d", - "created_date": "2024-08-16 23:58:35.113000", - "last_modified_date": "2024-08-16 23:58:35.113000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Mary Jane", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", - "created_date": "2024-08-16 23:58:34.826000", - "last_modified_date": "2024-08-16 23:58:34.826000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Amazing Fantasy", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "1b7de491-6bbb-404e-a2e5-a20a123e3fe5", - "created_date": "2024-08-16 23:58:35.135000", - "last_modified_date": "2024-08-16 23:58:35.135000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Mystic", - "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" - }, - { - "id": "1d9440ba-202a-4c4e-8425-8561606c1c3e", - "created_date": "2024-08-16 23:58:35.209000", - "last_modified_date": "2024-08-16 23:58:35.209000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Shrek", - "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" - }, - { - "id": "230efa0e-3aea-4b07-a05f-0f788f293d0b", - "created_date": "2024-08-16 23:58:35.320000", - "last_modified_date": "2024-08-16 23:58:35.320000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Witchblade / Tomb Raider", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "231e65eb-eecb-4946-a643-6184b767e321", - "created_date": "2024-08-16 23:58:34.840000", - "last_modified_date": "2024-08-16 23:58:34.840000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Aria", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "240a8a5d-eb07-4ce1-9fdd-a1b888c7426c", - "created_date": "2024-08-16 23:58:35.132000", - "last_modified_date": "2024-08-16 23:58:35.132000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Monster War 2005", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "25841b05-c246-4484-9ef2-71f8dcdb39ed", - "created_date": "2024-08-16 23:58:35.092000", - "last_modified_date": "2024-08-16 23:58:35.092000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Kiss Kiss Bang Bang", - "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" - }, - { - "id": "28adbced-8575-4858-8150-796857bb4129", - "created_date": "2024-08-16 23:58:35.336000", - "last_modified_date": "2024-08-16 23:58:35.336000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "X-Men: Age of Apocalypse One Shot", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "29dcafb5-2495-41d0-85df-6314e7bf8343", - "created_date": "2024-08-16 23:58:35.381000", - "last_modified_date": "2024-08-16 23:58:35.381000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Bomb Queen II: Queen of Hearts", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "2a4b287e-4b05-4016-8eb4-21fc225be24d", - "created_date": "2024-08-16 23:58:35.122000", - "last_modified_date": "2024-08-16 23:58:35.122000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Meridian", - "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" - }, - { - "id": "2ab84282-d7f5-43ef-af41-df0f756a62f7", - "created_date": "2024-08-16 23:58:35.376000", - "last_modified_date": "2024-08-16 23:58:35.376000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Army of Darkness: Shop Till You Drop Dead", - "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" - }, - { - "id": "2b2a07ba-c5a8-4cb8-b170-990cb941315a", - "created_date": "2024-08-16 23:58:35.218000", - "last_modified_date": "2024-08-16 23:58:35.218000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Soulfire", - "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" - }, - { - "id": "2c6ec683-8f23-4635-ab71-b377716213fc", - "created_date": "2024-08-16 23:58:35.396000", - "last_modified_date": "2024-08-16 23:58:35.396000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Star Wars: Knights of the Old Republic", - "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" - }, - { - "id": "2ca8751f-8d0a-449e-933f-f293e6fbd751", - "created_date": "2024-08-16 23:58:35.356000", - "last_modified_date": "2024-08-16 23:58:35.356000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Crux", - "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" - }, - { - "id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", - "created_date": "2024-08-16 23:58:35.267000", - "last_modified_date": "2024-08-16 23:58:35.267000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Tarot: Witch of the Black Rose", - "publisher_id": "587585ae-7002-4588-8716-f49d47ee05fc" - }, - { - "id": "3217d912-3b46-47d2-a6f8-a79c1eecfde1", - "created_date": "2024-08-16 23:58:35.277000", - "last_modified_date": "2024-08-16 23:58:35.277000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "The Devil\u00b4s Keeper", - "publisher_id": "479e4daf-f516-4eb1-af3a-ee9d4dcf5fee" - }, - { - "id": "33b14231-7f52-4a1d-8909-cb00e6a241ac", - "created_date": "2024-08-16 23:58:35.346000", - "last_modified_date": "2024-08-16 23:58:35.346000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "X-treme X-Men", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "34baf0ec-2af4-4ceb-9f54-ab92fb9a0b96", - "created_date": "2024-08-16 23:58:35.303000", - "last_modified_date": "2024-08-16 23:58:35.303000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Ultimate Spider-Man Annual", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "35894d94-1fb5-49de-ba77-cf2626cda939", - "created_date": "2024-08-16 23:58:35.143000", - "last_modified_date": "2024-08-16 23:58:35.143000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Necromancer", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "358d8ebf-8ada-4bc5-b47a-dd8a3b52b2c3", - "created_date": "2024-08-16 23:58:34.918000", - "last_modified_date": "2024-08-16 23:58:34.918000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Darkness / Tomb Raider", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "358f99c2-93f2-489d-83a4-03267fad8597", - "created_date": "2024-08-16 23:58:35.328000", - "last_modified_date": "2024-08-16 23:58:35.328000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Wraithborn", - "publisher_id": "38803fe4-26ef-40eb-8b2e-f718c6f27341" - }, - { - "id": "389378c4-d067-4f56-8842-6806b010b2d0", - "created_date": "2024-08-16 23:58:35.388000", - "last_modified_date": "2024-08-16 23:58:35.388000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Gen13", - "publisher_id": "38803fe4-26ef-40eb-8b2e-f718c6f27341" - }, - { - "id": "39536665-0c93-439f-97d4-be1f40da34dd", - "created_date": "2024-08-16 23:58:35.313000", - "last_modified_date": "2024-08-16 23:58:35.313000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Wildcats: Nemesis", - "publisher_id": "38803fe4-26ef-40eb-8b2e-f718c6f27341" - }, - { - "id": "39f1943d-0620-4d88-8709-c0bf8f35790d", - "created_date": "2024-08-16 23:58:35.006000", - "last_modified_date": "2024-08-16 23:58:35.006000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Freshmen", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "3e96be30-f58f-459d-bb97-cfa574b9487c", - "created_date": "2024-08-16 23:58:35.322000", - "last_modified_date": "2024-08-16 23:58:35.322000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Wolverine: The End", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "45bd0c8a-845f-4ab3-9281-c31e5a3d4472", - "created_date": "2024-08-16 23:58:35.236000", - "last_modified_date": "2024-08-16 23:58:35.236000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Spider-Man Team Up", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "45f57e1d-f3aa-40f7-b89a-b02eeda10577", - "created_date": "2024-08-16 23:58:35.325000", - "last_modified_date": "2024-08-16 23:58:35.325000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Wood Boy", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "46d02138-9fb9-4fbe-9928-d73c0faf2a4e", - "created_date": "2024-08-16 23:58:34.939000", - "last_modified_date": "2024-08-16 23:58:34.939000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Dream Police", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "46e41bf6-1c75-4e2b-9905-9dfda3bcfa9c", - "created_date": "2024-08-16 23:58:35.151000", - "last_modified_date": "2024-08-16 23:58:35.151000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "New Avengers", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "4a5c0ca1-9a08-49b9-bf7c-7bde4f35551a", - "created_date": "2024-08-16 23:58:34.879000", - "last_modified_date": "2024-08-16 23:58:34.879000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Birds of Prey", - "publisher_id": "c9efb32a-08ec-43bb-ba7c-95234146e96e" - }, - { - "id": "4b883248-716e-45b9-be2a-1eab276159bb", - "created_date": "2024-08-16 23:58:34.955000", - "last_modified_date": "2024-08-16 23:58:34.955000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Emma Frost", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "4f0e16a3-452f-43cd-9834-d0f4d2d5fca0", - "created_date": "2024-08-16 23:58:35.010000", - "last_modified_date": "2024-08-16 23:58:35.010000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Friendly Neighborhood Spider-Man", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "51bab5a9-915f-445c-8ab6-93de6120f88f", - "created_date": "2024-08-16 23:58:35.398000", - "last_modified_date": "2024-08-16 23:58:35.398000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Star Wars: Legacy", - "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" - }, - { - "id": "52f0849c-3c1c-49a0-9448-8b5b2c9b0c0c", - "created_date": "2024-08-16 23:58:35.270000", - "last_modified_date": "2024-08-16 23:58:35.270000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "The Art of Greg Horn", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "54ce47f2-d611-4d22-9ef5-c57e6d3e5967", - "created_date": "2024-08-16 23:58:34.900000", - "last_modified_date": "2024-08-16 23:58:34.900000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Crossgen", - "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" - }, - { - "id": "56152b4f-9a84-40ea-a329-8a267d931182", - "created_date": "2024-08-16 23:58:34.898000", - "last_modified_date": "2024-08-16 23:58:34.898000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Crimson", - "publisher_id": "38803fe4-26ef-40eb-8b2e-f718c6f27341" - }, - { - "id": "571cfe31-a696-41ca-8c28-ac68b17909ff", - "created_date": "2024-08-16 23:58:35.105000", - "last_modified_date": "2024-08-16 23:58:35.105000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Magdalena / Vampirella 2", - "publisher_id": "39b5fdd2-cfd1-4a9d-a8f3-6f011f6ce16a" - }, - { - "id": "57965b27-1330-4921-8c0b-4b09ee06084f", - "created_date": "2024-08-16 23:58:35.293000", - "last_modified_date": "2024-08-16 23:58:35.293000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Tomb Raider", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "5a72a63d-8fbd-46a8-b201-9b6e035c782a", - "created_date": "2024-08-16 23:58:34.861000", - "last_modified_date": "2024-08-16 23:58:34.861000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Athena Inc. The Manhunter Project", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "5aa143a9-d0a1-457f-b178-c8c71951dd91", - "created_date": "2024-08-16 23:58:35.301000", - "last_modified_date": "2024-08-16 23:58:35.301000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Ultimate Fantastic Four", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "5d0fd720-7875-4f9f-86eb-07ee0723908f", - "created_date": "2024-08-16 23:58:35.310000", - "last_modified_date": "2024-08-16 23:58:35.310000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Wild Girl", - "publisher_id": "38803fe4-26ef-40eb-8b2e-f718c6f27341" - }, - { - "id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", - "created_date": "2024-08-16 23:58:35.025000", - "last_modified_date": "2024-08-16 23:58:35.025000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Gift", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "5dbe4d8b-331a-41ad-bcdc-01196dc1d58d", - "created_date": "2024-08-16 23:58:35.343000", - "last_modified_date": "2024-08-16 23:58:35.343000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "X-Men: Phoenix - Endsong", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "5f648121-c503-46df-8a2b-56c11f5be6b4", - "created_date": "2024-08-16 23:58:34.959000", - "last_modified_date": "2024-08-16 23:58:34.959000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Excalibur", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "5fc6600f-b005-4d6b-a1af-a17cc2701d81", - "created_date": "2024-08-16 23:58:35.119000", - "last_modified_date": "2024-08-16 23:58:35.119000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Megacity 909", - "publisher_id": "1c3e0871-ca17-4766-935d-15e0ec33a5ac" - }, - { - "id": "60deca87-7b2a-4412-855f-01a6ccaeea56", - "created_date": "2024-08-16 23:58:35.340000", - "last_modified_date": "2024-08-16 23:58:35.340000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "X-Men: Kitty Pryde", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", - "created_date": "2024-08-16 23:58:34.892000", - "last_modified_date": "2024-08-16 23:58:34.892000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Brath", - "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" - }, - { - "id": "61a4d2fe-44b2-41bb-a19c-f7be95d5e195", - "created_date": "2024-08-16 23:58:34.967000", - "last_modified_date": "2024-08-16 23:58:34.967000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Fathom Beginnings", - "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" - }, - { - "id": "639eed1d-3ccf-4bfa-a595-06b44a4e5b8f", - "created_date": "2024-08-16 23:58:35.299000", - "last_modified_date": "2024-08-16 23:58:35.299000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Toxin", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "639f7e71-1012-49a6-bc3a-1ac7b1de3084", - "created_date": "2024-08-16 23:58:35.372000", - "last_modified_date": "2024-08-16 23:58:35.372000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Army of Darkness: Ashes 2 Ashes", - "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" - }, - { - "id": "63cfc38f-5f4e-4273-a630-7b455868687b", - "created_date": "2024-08-16 23:58:34.963000", - "last_modified_date": "2024-08-16 23:58:34.963000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Fathom", - "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" - }, - { - "id": "6d3e9abd-1c42-4024-903f-6b139571da25", - "created_date": "2024-08-16 23:58:35.049000", - "last_modified_date": "2024-08-16 23:58:35.049000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Harry Johnson", - "publisher_id": "c714d1b6-465f-4549-ba8d-0f2753863c4d" - }, - { - "id": "6f341583-4a3b-4d9a-b928-124f024fa005", - "created_date": "2024-08-16 23:58:35.188000", - "last_modified_date": "2024-08-16 23:58:35.188000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Rogue", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "6fd2c6fd-f8f1-4f13-892d-887b315fa4a3", - "created_date": "2024-08-16 23:58:34.929000", - "last_modified_date": "2024-08-16 23:58:34.929000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Darkness Vol. 2", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "6fff100e-050b-4da7-bdfa-10675dbab84e", - "created_date": "2024-08-16 23:58:35.282000", - "last_modified_date": "2024-08-16 23:58:35.282000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "The Tomb of Dracula", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "799309bc-d8d9-4d44-9457-bae7f1e7fcbf", - "created_date": "2024-08-16 23:58:35.351000", - "last_modified_date": "2024-08-16 23:58:35.351000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Army of Darkness vs. Re-Animator", - "publisher_id": "c96d450d-5034-435b-9afe-c49c937b6328" - }, - { - "id": "7c110b15-bbfc-472e-830b-b8db6ddb274e", - "created_date": "2024-08-16 23:58:35.333000", - "last_modified_date": "2024-08-16 23:58:35.333000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "X-Men: Age of Apocalypse", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "7e1bd46e-781b-4f6b-958b-0e9694cb7748", - "created_date": "2024-08-16 23:58:35.386000", - "last_modified_date": "2024-08-16 23:58:35.386000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Bomb Queen IV: Suicide Bomber", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "80944d0a-93fc-475b-bfb7-deed8c977832", - "created_date": "2024-08-16 23:58:35.392000", - "last_modified_date": "2024-08-16 23:58:35.392000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Iron & The Maiden", - "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" - }, - { - "id": "80f818f1-3813-4bf9-9eb2-0a441799fa6d", - "created_date": "2024-08-16 23:58:34.865000", - "last_modified_date": "2024-08-16 23:58:34.865000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Barbarossa & The Lost Corsairs", - "publisher_id": "9768502e-f0ab-446a-b095-2f363ff38c1c" - }, - { - "id": "82db30ac-0622-4785-9a43-95ae37e54eaa", - "created_date": "2024-08-16 23:58:35.085000", - "last_modified_date": "2024-08-16 23:58:35.085000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Iron Ghost", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "8441157d-4109-4dcd-a8a6-68b241d669fb", - "created_date": "2024-08-16 23:58:35.379000", - "last_modified_date": "2024-08-16 23:58:35.379000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "X-Men", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "851bc748-2602-4f20-826f-a59a7087d11f", - "created_date": "2024-08-16 23:58:34.823000", - "last_modified_date": "2024-08-16 23:58:34.823000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Abadazad", - "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" - }, - { - "id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", - "created_date": "2024-08-16 23:58:35.318000", - "last_modified_date": "2024-08-16 23:58:35.318000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Witchblade", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "8690631a-e99c-44be-887c-8fd2bb222ee1", - "created_date": "2024-08-16 23:58:35.102000", - "last_modified_date": "2024-08-16 23:58:35.102000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Lullaby", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "877c8105-ea6c-4624-b458-60d18b608c15", - "created_date": "2024-08-16 23:58:35.001000", - "last_modified_date": "2024-08-16 23:58:35.001000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Flak Riot", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "8955b2a3-84d3-4b55-a1f1-d45193600861", - "created_date": "2024-08-16 23:58:35.243000", - "last_modified_date": "2024-08-16 23:58:35.243000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Spider-Man: House of M", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "89c5ea13-997a-4831-87cc-ee76ea05c71e", - "created_date": "2024-08-16 23:58:34.885000", - "last_modified_date": "2024-08-16 23:58:34.885000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Black Widow 2", - "publisher_id": "978ef035-20af-4d89-b898-98159d8ce280" - }, - { - "id": "8a4558ac-33e9-4656-ab47-8292af313ff7", - "created_date": "2024-08-16 23:58:34.876000", - "last_modified_date": "2024-08-16 23:58:34.876000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Battle Pope", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "8a64d236-0f01-47cd-a841-6cc3fd6581ed", - "created_date": "2024-08-16 23:58:35.394000", - "last_modified_date": "2024-08-16 23:58:35.394000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Star Wars: Rebellion", - "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" - }, - { - "id": "8ad36a79-9436-455d-8096-0f1b73c22f13", - "created_date": "2024-08-16 23:58:35.195000", - "last_modified_date": "2024-08-16 23:58:35.195000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Scion", - "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" - }, - { - "id": "8e293af3-05c6-4dcc-9cbf-b87512ec975b", - "created_date": "2024-08-16 23:58:34.991000", - "last_modified_date": "2024-08-16 23:58:34.991000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Fathom Vol. 2", - "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" - }, - { - "id": "94837af3-bbaf-4496-b114-1676a3271cb6", - "created_date": "2024-08-16 23:58:34.813000", - "last_modified_date": "2024-08-16 23:58:34.813000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "1602", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "97685762-f1ae-4588-913f-0bdc38365360", - "created_date": "2024-08-16 23:58:34.974000", - "last_modified_date": "2024-08-16 23:58:34.974000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Fathom Cannon Hawke Prelude", - "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" - }, - { - "id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "created_date": "2024-08-16 23:58:35.139000", - "last_modified_date": "2024-08-16 23:58:35.139000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Mystique", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "9a175907-fea8-4f11-903f-6e837ce666c0", - "created_date": "2024-08-16 23:58:34.835000", - "last_modified_date": "2024-08-16 23:58:34.835000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Arana", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "9aac6484-4c06-4286-815a-219aad25cc74", - "created_date": "2024-08-16 23:58:35.058000", - "last_modified_date": "2024-08-16 23:58:35.058000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Hellcop", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "9b4d6ff0-f4f5-4bc6-8190-08755f35dbf1", - "created_date": "2024-08-16 23:58:34.888000", - "last_modified_date": "2024-08-16 23:58:34.888000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Bluntman and Chronic", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "9b924cdc-8959-41e0-a84d-f3e61bbeac44", - "created_date": "2024-08-16 23:58:35.021000", - "last_modified_date": "2024-08-16 23:58:35.021000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Ghostrider", - "publisher_id": "978ef035-20af-4d89-b898-98159d8ce280" - }, - { - "id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "created_date": "2024-08-16 23:58:35.214000", - "last_modified_date": "2024-08-16 23:58:35.214000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Sojourn", - "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" - }, - { - "id": "9fcdd9a5-f1fb-4421-a352-9da8e2c12f81", - "created_date": "2024-08-16 23:58:34.910000", - "last_modified_date": "2024-08-16 23:58:34.910000", - "version": 0, - "completed": 1, - "current_order": 0, - "title": "Daring Escapes", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "a08aef89-634c-494c-9def-73ff7e416464", - "created_date": "2024-08-16 23:58:35.067000", - "last_modified_date": "2024-08-16 23:58:35.067000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "House of M", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "created_date": "2024-08-16 23:58:35.224000", - "last_modified_date": "2024-08-16 23:58:35.224000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Spectacular Spider-Man", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "a2409ef1-82c3-45d1-9c65-b8806a31e525", - "created_date": "2024-08-16 23:58:35.182000", - "last_modified_date": "2024-08-16 23:58:35.182000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Red Sonja", - "publisher_id": "c96d450d-5034-435b-9afe-c49c937b6328" - }, - { - "id": "a557dbbc-2af1-4b56-8588-8fe42cf5c454", - "created_date": "2024-08-16 23:58:35.095000", - "last_modified_date": "2024-08-16 23:58:35.095000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Legend of Isis", - "publisher_id": "479e4daf-f516-4eb1-af3a-ee9d4dcf5fee" - }, - { - "id": "a5987e5c-0245-484d-aeb8-4b5195800d66", - "created_date": "2024-08-16 23:58:34.907000", - "last_modified_date": "2024-08-16 23:58:34.907000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Danger Girl Back in Black", - "publisher_id": "38803fe4-26ef-40eb-8b2e-f718c6f27341" - }, - { - "id": "ab909424-4ab4-4084-a47d-08ab865047e7", - "created_date": "2024-08-16 23:58:35.107000", - "last_modified_date": "2024-08-16 23:58:35.107000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Marvel Knights Spider-Man", - "publisher_id": "978ef035-20af-4d89-b898-98159d8ce280" - }, - { - "id": "ab9aa494-b791-498d-bf13-6c3c50c16667", - "created_date": "2024-08-16 23:58:35.291000", - "last_modified_date": "2024-08-16 23:58:35.291000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Tom Strong", - "publisher_id": "38803fe4-26ef-40eb-8b2e-f718c6f27341" - }, - { - "id": "abfcb11c-7757-4db8-879e-b0d1803819d9", - "created_date": "2024-08-16 23:58:35.154000", - "last_modified_date": "2024-08-16 23:58:35.154000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "New Mutants", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "af866a6a-2b51-499a-aa2d-b46743aabafd", - "created_date": "2024-08-16 23:58:35.250000", - "last_modified_date": "2024-08-16 23:58:35.250000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Stardust Kid", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", - "created_date": "2024-08-16 23:58:35.288000", - "last_modified_date": "2024-08-16 23:58:35.288000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Thor: Son of Asgard", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "b472b359-d586-458c-9042-a5fee057da3b", - "created_date": "2024-08-16 23:58:34.932000", - "last_modified_date": "2024-08-16 23:58:34.932000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "District X", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "b4c866cb-9461-4aa4-bc3b-ef3e63848775", - "created_date": "2024-08-16 23:58:35.360000", - "last_modified_date": "2024-08-16 23:58:35.360000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Aria: The Soul Market", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "b51c738d-8ba1-4451-8f89-f49c1968ac42", - "created_date": "2024-08-16 23:58:34.882000", - "last_modified_date": "2024-08-16 23:58:34.882000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Black Widow", - "publisher_id": "978ef035-20af-4d89-b898-98159d8ce280" - }, - { - "id": "b6383b7f-1c86-42d9-a3f6-d2a4bc96dc51", - "created_date": "2024-08-16 23:58:35.306000", - "last_modified_date": "2024-08-16 23:58:35.306000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Uncanny X-Men", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "b6713944-8d2d-4153-8f16-fe94cc4ee119", - "created_date": "2024-08-16 23:58:35.257000", - "last_modified_date": "2024-08-16 23:58:35.257000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Supergirl", - "publisher_id": "c9efb32a-08ec-43bb-ba7c-95234146e96e" - }, - { - "id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "created_date": "2024-08-16 23:58:35.014000", - "last_modified_date": "2024-08-16 23:58:35.014000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Futurama", - "publisher_id": "27b4304a-3bac-4ed3-9c2d-e8027d016dc0" - }, - { - "id": "b73e5e2f-60e4-42fe-94bf-44fe3755b8b8", - "created_date": "2024-08-16 23:58:35.193000", - "last_modified_date": "2024-08-16 23:58:35.193000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Samurai: Heaven & Earth", - "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" - }, - { - "id": "b9a8abf9-b259-4a6f-be2d-75f211788440", - "created_date": "2024-08-16 23:58:34.913000", - "last_modified_date": "2024-08-16 23:58:34.913000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Darkness / Superman", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "bba808d8-ede8-49fe-9ed5-3c23c7ca0f3c", - "created_date": "2024-08-16 23:58:34.980000", - "last_modified_date": "2024-08-16 23:58:34.980000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Fathom Prelude", - "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" - }, - { - "id": "be424be0-a91c-47b4-b4a9-1e1d760a7177", - "created_date": "2024-08-16 23:58:35.017000", - "last_modified_date": "2024-08-16 23:58:35.017000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Futurama Simpsons Crossover Crisis Part 2", - "publisher_id": "27b4304a-3bac-4ed3-9c2d-e8027d016dc0" - }, - { - "id": "becf411e-8fe2-470e-8bde-7991c59988e0", - "created_date": "2024-08-16 23:58:35.063000", - "last_modified_date": "2024-08-16 23:58:35.063000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Holiday Special 2004", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "bf23d317-f5b6-4cf8-8a05-4397888a82c9", - "created_date": "2024-08-16 23:58:34.895000", - "last_modified_date": "2024-08-16 23:58:34.895000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Catwoman When In Rome", - "publisher_id": "c9efb32a-08ec-43bb-ba7c-95234146e96e" - }, - { - "id": "bf59f643-455e-4b60-95b8-719d55437474", - "created_date": "2024-08-16 23:58:35.227000", - "last_modified_date": "2024-08-16 23:58:35.227000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Spellbinders", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "c03ae569-1a0b-4d17-8cfe-7e8972303751", - "created_date": "2024-08-16 23:58:35.383000", - "last_modified_date": "2024-08-16 23:58:35.383000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Bomb Queen III: The Good, The Bad and The Lovely", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "c0543c9f-d712-4dce-9b59-2bf73b800b31", - "created_date": "2024-08-16 23:58:35.110000", - "last_modified_date": "2024-08-16 23:58:35.110000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Marville", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "created_date": "2024-08-16 23:58:35.191000", - "last_modified_date": "2024-08-16 23:58:35.191000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Ruse", - "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" - }, - { - "id": "c0a044dd-461b-459a-9962-b94ed0e8b38f", - "created_date": "2024-08-16 23:58:34.986000", - "last_modified_date": "2024-08-16 23:58:34.986000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Fathom Swimsuit Special 2000", - "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" - }, - { - "id": "c686b9c2-abdc-4476-a530-ee85ce221a5d", - "created_date": "2024-08-16 23:58:35.233000", - "last_modified_date": "2024-08-16 23:58:35.233000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Spider-Man loves Mary Jane", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "c91ec109-0b4d-4fd6-995f-1a828958493f", - "created_date": "2024-08-16 23:58:35.254000", - "last_modified_date": "2024-08-16 23:58:35.254000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Strange", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "c9d4b26a-8431-4f68-8e7a-b61f2cf24176", - "created_date": "2024-08-16 23:58:35.164000", - "last_modified_date": "2024-08-16 23:58:35.164000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "New X-Men Hellions", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "cc96b25e-b827-4ff0-a94a-b82d30ca883c", - "created_date": "2024-08-16 23:58:35.157000", - "last_modified_date": "2024-08-16 23:58:35.157000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "New X-Men", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "ce879e17-f391-4de0-81c5-3279cbc87fc8", - "created_date": "2024-08-16 23:58:34.922000", - "last_modified_date": "2024-08-16 23:58:34.922000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Darkness / Vampirella", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "d16a57e0-5c70-4bdb-b71a-1b5290a838fd", - "created_date": "2024-08-16 23:58:35.401000", - "last_modified_date": "2024-08-16 23:58:35.401000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Star Wars: Dark Times", - "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" - }, - { - "id": "d23966db-c03d-4dd3-9f65-5aa18c689053", - "created_date": "2024-08-16 23:58:34.845000", - "last_modified_date": "2024-08-16 23:58:34.845000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Army of Darkness", - "publisher_id": "1c3e0871-ca17-4766-935d-15e0ec33a5ac" - }, - { - "id": "d24801e2-fbfe-4497-873f-4d8edb182ae4", - "created_date": "2024-08-16 23:58:35.261000", - "last_modified_date": "2024-08-16 23:58:35.261000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Superman", - "publisher_id": "c9efb32a-08ec-43bb-ba7c-95234146e96e" - }, - { - "id": "d33657a6-0ab7-4177-b5b3-4a0c9d358d03", - "created_date": "2024-08-16 23:58:35.147000", - "last_modified_date": "2024-08-16 23:58:35.147000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Negation War", - "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" - }, - { - "id": "d37740d1-9c0d-480f-bf14-16f868f50a2c", - "created_date": "2024-08-16 23:58:35.247000", - "last_modified_date": "2024-08-16 23:58:35.247000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Star Wars", - "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" - }, - { - "id": "d7dd6d02-bc9a-4fbf-a5ca-ad4728cb8109", - "created_date": "2024-08-16 23:58:34.873000", - "last_modified_date": "2024-08-16 23:58:34.873000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Bart Simpsons Treehouse of Horror", - "publisher_id": "27b4304a-3bac-4ed3-9c2d-e8027d016dc0" - }, - { - "id": "d99f789a-0d9c-4b12-bf1f-3b090fb0f1b8", - "created_date": "2024-08-16 23:58:34.948000", - "last_modified_date": "2024-08-16 23:58:34.948000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "El Cazador The Bloody Ballad of Blackjack Tom", - "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" - }, - { - "id": "db80cac0-8598-4063-9a5f-cd4f9c0f457c", - "created_date": "2024-08-16 23:58:34.926000", - "last_modified_date": "2024-08-16 23:58:34.926000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Darkness Black Sails", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "dc81f255-6757-4c41-8a7b-a06a503d7daa", - "created_date": "2024-08-16 23:58:34.983000", - "last_modified_date": "2024-08-16 23:58:34.983000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Fathom Swimsuit Special", - "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" - }, - { - "id": "de7ef7f5-daf8-4dfd-b8de-973c902a7df0", - "created_date": "2024-08-16 23:58:34.952000", - "last_modified_date": "2024-08-16 23:58:34.952000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Elsinore", - "publisher_id": "479e4daf-f516-4eb1-af3a-ee9d4dcf5fee" - }, - { - "id": "dea3ab08-5e3e-4438-b28f-fcb711a3b593", - "created_date": "2024-08-16 23:58:35.128000", - "last_modified_date": "2024-08-16 23:58:35.128000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Monster War", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "df47cdb1-0b41-4baa-83ce-fcfbb1c2bd51", - "created_date": "2024-08-16 23:58:35.071000", - "last_modified_date": "2024-08-16 23:58:35.071000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Hunter-Killer", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "e048cec2-52b9-48f8-9975-cfe17ed85aae", - "created_date": "2024-08-16 23:58:35.285000", - "last_modified_date": "2024-08-16 23:58:35.285000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Robert Jordan\u00b4s The Wheel of Time: New Spring", - "publisher_id": "ab7c10bf-8cd0-4a77-823f-a6764e9033d4" - }, - { - "id": "e1ff1410-ac3a-4ba5-8503-1fab529e50c0", - "created_date": "2024-08-16 23:58:35.186000", - "last_modified_date": "2024-08-16 23:58:35.186000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Revelations", - "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" - }, - { - "id": "e2e7a53a-fbd5-473c-9409-3acb87247728", - "created_date": "2024-08-16 23:58:35.169000", - "last_modified_date": "2024-08-16 23:58:35.169000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Nightcrawler", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "e4002ab9-0f26-48e6-9927-63c350df8015", - "created_date": "2024-08-16 23:58:34.858000", - "last_modified_date": "2024-08-16 23:58:34.858000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Athena Inc. The Beginning", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "e54ebebe-701a-4d60-88ce-4df9b34da6ca", - "created_date": "2024-08-16 23:58:35.200000", - "last_modified_date": "2024-08-16 23:58:35.200000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "She-Hulk", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "e6b93088-b350-412b-9634-227216ff252a", - "created_date": "2024-08-16 23:58:35.043000", - "last_modified_date": "2024-08-16 23:58:35.043000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Hack/Slash: Girls Gone Dead", - "publisher_id": "1c3e0871-ca17-4766-935d-15e0ec33a5ac" - }, - { - "id": "ea903b99-4032-4b4e-add4-177e051733a8", - "created_date": "2024-08-16 23:58:35.029000", - "last_modified_date": "2024-08-16 23:58:35.029000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Hack Slash Land of Lost Toys", - "publisher_id": "1c3e0871-ca17-4766-935d-15e0ec33a5ac" - }, - { - "id": "ed58b16e-0701-4373-befe-39118bc2d4cb", - "created_date": "2024-08-16 23:58:35.264000", - "last_modified_date": "2024-08-16 23:58:35.264000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Superman/Batman", - "publisher_id": "c9efb32a-08ec-43bb-ba7c-95234146e96e" - }, - { - "id": "ef71fb0f-ecba-4d88-ae5f-90c81b0c4e18", - "created_date": "2024-08-16 23:58:35.316000", - "last_modified_date": "2024-08-16 23:58:35.316000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Wildsiderz", - "publisher_id": "38803fe4-26ef-40eb-8b2e-f718c6f27341" - }, - { - "id": "efc52177-93a0-4e69-a76f-2c9049ff3967", - "created_date": "2024-08-16 23:58:35.178000", - "last_modified_date": "2024-08-16 23:58:35.178000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Radix", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", - "created_date": "2024-08-16 23:58:35.211000", - "last_modified_date": "2024-08-16 23:58:35.211000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Simpsons", - "publisher_id": "27b4304a-3bac-4ed3-9c2d-e8027d016dc0" - }, - { - "id": "f3231681-cd2b-4ff9-bfa2-5d8f631bee4d", - "created_date": "2024-08-16 23:58:35.117000", - "last_modified_date": "2024-08-16 23:58:35.117000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Mary Jane Homecoming", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "f49085fd-c407-4aa8-bc57-118dde083369", - "created_date": "2024-08-16 23:58:35.125000", - "last_modified_date": "2024-08-16 23:58:35.125000", - "version": 0, - "completed": 1, - "current_order": 0, - "title": "Midnight Nation", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "f4cb5b24-00ea-4249-9d09-45432c168b8f", - "created_date": "2024-08-16 23:58:35.221000", - "last_modified_date": "2024-08-16 23:58:35.221000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Soulfire Dying of the Light", - "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" - }, - { - "id": "f4f227f0-a6d1-49fd-baea-6791245a89e7", - "created_date": "2024-08-16 23:58:34.970000", - "last_modified_date": "2024-08-16 23:58:34.970000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Fathom Cannon Hawke", - "publisher_id": "61f5f1e5-dc60-4d24-be2d-352baa449c65" - }, - { - "id": "f6e4b186-dca5-46f6-8b4d-eb52b32265e5", - "created_date": "2024-08-16 23:58:34.818000", - "last_modified_date": "2024-08-16 23:58:34.818000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "10th Muse", - "publisher_id": "479e4daf-f516-4eb1-af3a-ee9d4dcf5fee" - }, - { - "id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "created_date": "2024-08-16 23:58:34.830000", - "last_modified_date": "2024-08-16 23:58:34.830000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Amazing Spider-Man", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "fa203e3e-db4a-44ed-beab-df526fec838c", - "created_date": "2024-08-16 23:58:35.082000", - "last_modified_date": "2024-08-16 23:58:35.082000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Hunter-Killer Dossier", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "fd313197-70f1-45d5-8ca3-c8b6d828254e", - "created_date": "2024-08-16 23:58:35.203000", - "last_modified_date": "2024-08-16 23:58:35.203000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "She-Hulk 2", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "fdcbf2b1-c3cb-44d8-888b-41260a87b0e4", - "created_date": "2024-08-16 23:58:35.296000", - "last_modified_date": "2024-08-16 23:58:35.296000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Tomb Raider: The Greatest Treasure of All", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - }, - { - "id": "fdccf7d0-db2c-4b01-a412-66e3ff043fe9", - "created_date": "2024-08-16 23:58:35.173000", - "last_modified_date": "2024-08-16 23:58:35.173000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Ororo: Before the Storm", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "fe69cf80-946a-45d6-9fba-55ebfc0f038c", - "created_date": "2024-08-16 23:58:35.098000", - "last_modified_date": "2024-08-16 23:58:35.098000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Loki", - "publisher_id": "083ae601-ad41-4d5f-b068-bf09e437675b" - }, - { - "id": "fe7fb34e-5ff2-4f6d-a649-0be19a368f46", - "created_date": "2024-08-16 23:58:35.207000", - "last_modified_date": "2024-08-16 23:58:35.207000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Shi Ju-Nen", - "publisher_id": "65330a74-7985-4f53-9316-27abc48259d5" - }, - { - "id": "ff81bcf6-b368-4264-872c-544e85ec80e8", - "created_date": "2024-08-16 23:58:35.216000", - "last_modified_date": "2024-08-16 23:58:35.216000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "Solus", - "publisher_id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306" - }, - { - "id": "fffaa1a6-5c3d-4deb-b759-35de74c65958", - "created_date": "2024-08-16 23:58:35.088000", - "last_modified_date": "2024-08-16 23:58:35.088000", - "version": 0, - "completed": 0, - "current_order": 0, - "title": "J.U.D.G.E.: Secret Rage", - "publisher_id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b" - } - ], - "media_video": [ - { - "id": "0aa9240c-3de9-4818-82d3-327f8103a36d", - "created_date": "2024-09-05 19:19:00.839024", - "last_modified_date": "2024-09-05 19:19:00.839024", - "version": 0, - "url": "https://www.youtube.com/watch?v=ReaFJ_MD2cs", - "review": 0, - "should_download": 1, - "title": "BLIND GUARDIAN - Secrets Of The American Gods (OFFICIAL MUSIC VIDEO) - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "2d8d3587-a382-42e2-8db2-8b2a83b1afa1", - "created_date": "2024-09-05 19:19:00.915634", - "last_modified_date": "2024-09-05 19:19:00.915634", - "version": 0, - "url": "https://www.youtube.com/watch?v=shDNKBMd6aw", - "review": 0, - "should_download": 1, - "title": "Nightwish & Floor Jansen - Song of Myself (Live @ Wacken 2013) - Lyric Video - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "3299aa51-0f99-4984-9f44-dcdde5098b5b", - "created_date": "2024-09-05 19:19:00.879147", - "last_modified_date": "2024-09-05 19:19:00.879147", - "version": 0, - "url": "https://www.youtube.com/watch?v=oSnuV_QmB-A", - "review": 0, - "should_download": 1, - "title": "Versengold - Die wilde Jagd (Offizielles Video) - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "33802d64-0711-4641-aeb3-842e06cc3aa7", - "created_date": "2024-09-05 19:19:00.943202", - "last_modified_date": "2024-09-05 19:19:00.943202", - "version": 0, - "url": "https://www.youtube.com/watch?v=oHCaZmIzr0o", - "review": 0, - "should_download": 1, - "title": "Nightwish - Perfume Of The Timeless (OFFICIAL MUSIC VIDEO) - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "3be2ab93-10b5-4c05-89c9-78ffa95725f2", - "created_date": "2024-09-05 19:19:00.926695", - "last_modified_date": "2024-09-05 19:19:00.926695", - "version": 0, - "url": "https://www.youtube.com/watch?v=gM0ol6y-qjQ", - "review": 0, - "should_download": 1, - "title": "Saltatio Mortis - Der Himmel muss warten (Official Lyric Video) - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "3f9dabea-fec1-40e2-bc62-2724d3cf816f", - "created_date": "2024-09-05 19:19:00.874536", - "last_modified_date": "2024-09-05 19:19:00.874536", - "version": 0, - "url": "https://www.youtube.com/watch?v=YWWp4uL53ho", - "review": 0, - "should_download": 1, - "title": "BLIND GUARDIAN - Ashes to Ashes (Live at Hellfest 2022) (OFFICIAL MUSIC VIDEO) - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "3fcbb868-8914-40df-b009-ff1361130c5d", - "created_date": "2024-09-05 19:19:00.890921", - "last_modified_date": "2024-09-05 19:19:00.890921", - "version": 0, - "url": "https://www.youtube.com/watch?v=J86zYZGApkc", - "review": 0, - "should_download": 1, - "title": "Die Toten Hosen // \u201a\u00c4\u00fbSteh auf, wenn du am Boden bist\u201a\u00c4\u00fa [Offizielles Musikvideo] - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "4062cb00-d436-4812-8fe2-892263edea35", - "created_date": "2024-09-05 19:19:00.900764", - "last_modified_date": "2024-09-05 19:19:00.900764", - "version": 0, - "url": "https://www.youtube.com/watch?v=zOvsyamoEDg", - "review": 0, - "should_download": 1, - "title": "FAUN - Federkleid (Offizielles Video) - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "4973e4a0-a9f9-4cd8-afa0-38699ee33498", - "created_date": "2024-09-05 19:19:00.824257", - "last_modified_date": "2024-09-05 19:19:00.824257", - "version": 0, - "url": "https://www.youtube.com/watch?v=zNHGJqgHvOU", - "review": 0, - "should_download": 1, - "title": "Blind Guardian - The Bard\u2018s Song - Live Wacken Open Air 2024 - 02.08.2024 - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "4e4510ca-70f8-441a-a61d-e62a1fb293d8", - "created_date": "2024-09-05 19:19:00.931969", - "last_modified_date": "2024-09-05 19:19:00.931969", - "version": 0, - "url": "https://www.youtube.com/watch?v=jWXsSwAMSHA", - "review": 0, - "should_download": 1, - "title": "Feuerschwanz - Schubsetanz - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "4e9ffca5-f7ea-460e-9e97-0985dd27cd42", - "created_date": "2024-09-16 13:59:16.510000", - "last_modified_date": "2024-09-16 13:59:16.510000", - "version": 0, - "url": "https://www.youtube.com/watch?v=yTypkLfDgMo", - "review": 0, - "should_download": 1, - "title": "FEUERSCHWANZ - Das Elfte Gebot (Official Video) | Napalm Records - YouTube", - "file_name": "", - "path": null, - "cloud_link": "" - }, - { - "id": "575801fa-40bb-472b-bc2d-6ba46e1424c9", - "created_date": "2024-09-05 19:19:00.971650", - "last_modified_date": "2024-09-05 19:19:00.971650", - "version": 0, - "url": "https://www.youtube.com/watch?v=5iz8dt3tknw", - "review": 0, - "should_download": 1, - "title": "Saltatio Mortis - Feuer und Erz (Official Video) - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "599d0001-86e9-49c3-901f-9240e1201078", - "created_date": "2024-09-05 19:19:00.911214", - "last_modified_date": "2024-09-05 20:12:13.227000", - "version": 1, - "url": "https://www.youtube.com/watch?v=VGko10RIGtY", - "review": 0, - "should_download": 1, - "title": "VERSENGOLD - Hoch die Kr\u00fcge (offizielles Video) | Zeitlos - YouTube", - "file_name": "", - "path": null, - "cloud_link": "" - }, - { - "id": "5a62e207-ebe9-4b5f-9a34-be34eaa533f1", - "created_date": "2024-09-05 19:19:00.895556", - "last_modified_date": "2024-09-05 19:19:00.895556", - "version": 0, - "url": "https://www.youtube.com/watch?v=sItCRENB1hU", - "review": 0, - "should_download": 1, - "title": "Faun ~ Wenn wir uns wiedersehen (Von Den Elben) - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "6c52cbea-a26f-4478-b77a-d7f71f7f3f54", - "created_date": "2024-09-05 19:19:00.960161", - "last_modified_date": "2024-09-05 19:19:00.960161", - "version": 0, - "url": "https://www.youtube.com/watch?v=09K4HJ31xj4", - "review": 0, - "should_download": 1, - "title": "Saltatio Mortis - Schwarzer Strand feat. Faun (Official Music Video) - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "84b32b93-5dde-4729-877c-6c8e1011d329", - "created_date": "2024-09-05 19:19:00.905726", - "last_modified_date": "2024-09-05 19:19:00.905726", - "version": 0, - "url": "https://www.youtube.com/watch?v=-J4AuEj4zHE", - "review": 0, - "should_download": 1, - "title": "Faun - Feuer (Offizielles Video) - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "856547c8-9863-47b9-85e3-9f3bfab3e0da", - "created_date": "2024-09-05 19:19:00.965710", - "last_modified_date": "2024-09-05 19:19:00.965710", - "version": 0, - "url": "https://www.youtube.com/watch?v=plMQ2r0z0K8", - "review": 0, - "should_download": 1, - "title": "Saltatio Mortis - Finsterwacht feat. Blind Guardian (Official Video) - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "88097439-8c05-4f19-ba5a-b81d1440bb01", - "created_date": "2024-09-05 19:19:00.954452", - "last_modified_date": "2024-09-05 19:19:00.954452", - "version": 0, - "url": "https://www.youtube.com/watch?v=pfrZE6JhJXU", - "review": 0, - "should_download": 1, - "title": "Schwarzer Strand | Saltatio Mortis X Faun | Making of - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "8ea48c73-02ea-43a3-9c9e-2800af71a132", - "created_date": "2024-09-16 13:59:36.878000", - "last_modified_date": "2024-09-16 13:59:36.878000", - "version": 0, - "url": "https://www.youtube.com/watch?v=WmlshlqXD54", - "review": 0, - "should_download": 1, - "title": "FEUERSCHWANZ ft. Melissa Bonny - Ding (SEEED Cover) | Napalm Records - YouTube", - "file_name": "", - "path": null, - "cloud_link": "" - }, - { - "id": "9378d488-4c4f-4fc5-acf9-25a44c4415b5", - "created_date": "2024-09-05 19:19:00.869366", - "last_modified_date": "2024-09-05 19:19:00.869366", - "version": 0, - "url": "https://www.youtube.com/watch?v=JyfE55c_ZjI", - "review": 0, - "should_download": 1, - "title": "BLIND GUARDIAN - Sacred Worlds (Sacred 2 - In-Game Concert) (OFFICIAL VIDEO) - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "958e1183-b901-446c-a596-0f41792486b4", - "created_date": "2024-09-05 19:19:00.885400", - "last_modified_date": "2024-09-05 19:19:00.885400", - "version": 0, - "url": "https://www.youtube.com/watch?v=nLgM1QJ3S_I", - "review": 0, - "should_download": 1, - "title": "FAUN - Walpurgisnacht (Official Video) - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "95eff153-4b89-46b7-a152-e482361293ac", - "created_date": "2024-09-05 19:19:00.832467", - "last_modified_date": "2024-09-05 19:19:00.832467", - "version": 0, - "url": "https://www.youtube.com/watch?v=fvtZ9_EN0xs", - "review": 0, - "should_download": 1, - "title": "BLIND GUARDIAN - The Quest for Tanelorn (Revisited) (OFFICIAL MUSIC VIDEO) - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "99822fe5-b1f6-4e9a-abd5-7a44d3a3867f", - "created_date": "2024-09-05 19:19:00.921273", - "last_modified_date": "2024-09-06 07:29:17.414000", - "version": 1, - "url": "https://www.ardmediathek.de/sendung/campervan-roadtrip/Y3JpZDovL2hyLW9ubGluZS8zODIyMDIxOQ", - "review": 0, - "should_download": 0, - "title": "Campervan-Roadtrip - alle verf\u00fcgbaren Videos - jetzt streamen!", - "file_name": "", - "path": null, - "cloud_link": "" - }, - { - "id": "a0c2c24d-7902-4f96-9d1e-ab9e48c93afe", - "created_date": "2024-09-05 19:19:00.864414", - "last_modified_date": "2024-09-05 19:19:00.864414", - "version": 0, - "url": "https://www.youtube.com/watch?v=SVg8eP7KPNQ", - "review": 0, - "should_download": 1, - "title": "BLIND GUARDIAN - Mirror Mirror (OFFICIAL LIVE VIDEO) - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "a8e549ac-aab7-4b39-891a-f594c581ab06", - "created_date": "2024-09-05 19:19:00.948913", - "last_modified_date": "2024-09-05 19:19:00.948913", - "version": 0, - "url": "https://www.youtube.com/watch?v=SOBzj0CZgiE", - "review": 0, - "should_download": 1, - "title": "Making of - Kaufmann und Maid | Behind the scenes | Saltatio Mortis - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "d6928639-8341-4d47-8af8-6ce86edebbd8", - "created_date": "2024-09-05 19:19:00.858012", - "last_modified_date": "2024-09-05 19:19:00.858012", - "version": 0, - "url": "https://www.youtube.com/watch?v=eIZNb96EQJ8", - "review": 0, - "should_download": 1, - "title": "BLIND GUARDIAN - A Voice In The Dark (OFFICIAL MUSIC VIDEO) - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - }, - { - "id": "f19e4f0d-e561-4313-b9d4-fec2277904fc", - "created_date": "2024-09-05 19:19:00.937716", - "last_modified_date": "2024-09-05 19:19:00.937716", - "version": 0, - "url": "https://www.youtube.com/watch?v=vaT0vAVBCMo", - "review": 0, - "should_download": 1, - "title": "FAUN - BLOT (Official Video) - YouTube", - "file_name": null, - "path": null, - "cloud_link": null - } - ], - "publisher": [ - { - "id": "083ae601-ad41-4d5f-b068-bf09e437675b", - "created_date": "2024-08-16 23:58:34.705000", - "last_modified_date": "2024-08-16 23:58:34.705000", - "version": 0, - "name": "Marvel" - }, - { - "id": "1c3e0871-ca17-4766-935d-15e0ec33a5ac", - "created_date": "2024-08-16 23:58:34.723000", - "last_modified_date": "2024-08-16 23:58:34.723000", - "version": 0, - "name": "Devils Due Publishing" - }, - { - "id": "27b4304a-3bac-4ed3-9c2d-e8027d016dc0", - "created_date": "2024-08-16 23:58:34.729000", - "last_modified_date": "2024-08-16 23:58:34.729000", - "version": 0, - "name": "Bongo Comics" - }, - { - "id": "38803fe4-26ef-40eb-8b2e-f718c6f27341", - "created_date": "2024-08-16 23:58:34.745000", - "last_modified_date": "2024-08-16 23:58:34.745000", - "version": 0, - "name": "WildStorm" - }, - { - "id": "39b5fdd2-cfd1-4a9d-a8f3-6f011f6ce16a", - "created_date": "2024-08-16 23:58:34.772000", - "last_modified_date": "2024-08-16 23:58:34.772000", - "version": 0, - "name": "Top Cow Productions" - }, - { - "id": "40a7080b-25a7-4988-a0e8-5dd1d5a73306", - "created_date": "2024-08-16 23:58:34.715000", - "last_modified_date": "2024-08-16 23:58:34.715000", - "version": 0, - "name": "Crossgen" - }, - { - "id": "46cef536-96d3-442a-a871-fd133050053e", - "created_date": "2024-08-16 23:58:34.750000", - "last_modified_date": "2024-08-16 23:58:34.750000", - "version": 0, - "name": "Cliffhanger" - }, - { - "id": "479e4daf-f516-4eb1-af3a-ee9d4dcf5fee", - "created_date": "2024-08-16 23:58:34.711000", - "last_modified_date": "2024-08-16 23:58:34.711000", - "version": 0, - "name": "Alias" - }, - { - "id": "587585ae-7002-4588-8716-f49d47ee05fc", - "created_date": "2024-08-16 23:58:34.760000", - "last_modified_date": "2024-08-16 23:58:34.760000", - "version": 0, - "name": "Broadsword" - }, - { - "id": "61f5f1e5-dc60-4d24-be2d-352baa449c65", - "created_date": "2024-08-16 23:58:34.726000", - "last_modified_date": "2024-08-16 23:58:34.726000", - "version": 0, - "name": "Aspen" - }, - { - "id": "65330a74-7985-4f53-9316-27abc48259d5", - "created_date": "2024-08-16 23:58:34.754000", - "last_modified_date": "2024-08-16 23:58:34.754000", - "version": 0, - "name": "Dark Horse Comics" - }, - { - "id": "9768502e-f0ab-446a-b095-2f363ff38c1c", - "created_date": "2024-08-16 23:58:34.733000", - "last_modified_date": "2024-08-16 23:58:34.733000", - "version": 0, - "name": "Kandora" - }, - { - "id": "978ef035-20af-4d89-b898-98159d8ce280", - "created_date": "2024-08-16 23:58:34.741000", - "last_modified_date": "2024-08-16 23:58:34.741000", - "version": 0, - "name": "Marvel Knights" - }, - { - "id": "ab7c10bf-8cd0-4a77-823f-a6764e9033d4", - "created_date": "2024-08-16 23:58:34.768000", - "last_modified_date": "2024-08-16 23:58:34.768000", - "version": 0, - "name": "Red Eagle Entertainment" - }, - { - "id": "c714d1b6-465f-4549-ba8d-0f2753863c4d", - "created_date": "2024-08-16 23:58:34.775000", - "last_modified_date": "2024-08-16 23:58:34.775000", - "version": 0, - "name": "Pulp Fiction" - }, - { - "id": "c96d450d-5034-435b-9afe-c49c937b6328", - "created_date": "2024-08-16 23:58:34.764000", - "last_modified_date": "2024-08-16 23:58:34.764000", - "version": 0, - "name": "Dynamite Entertainment" - }, - { - "id": "c9efb32a-08ec-43bb-ba7c-95234146e96e", - "created_date": "2024-08-16 23:58:34.737000", - "last_modified_date": "2024-08-16 23:58:34.737000", - "version": 0, - "name": "DC" - }, - { - "id": "e6ec31d7-20c1-453a-9b55-a8f78b2c5d8b", - "created_date": "2024-08-16 23:58:34.718000", - "last_modified_date": "2024-08-16 23:58:34.718000", - "version": 0, - "name": "Image" - } - ], - "authorization_matrix": [ - { - "id": "02e76146-7741-4ad0-bb18-1f1697f4637c", - "created_date": "2024-08-16 23:58:34.662000", - "last_modified_date": "2024-08-16 23:58:34.662000", - "version": 0, - "user_id": "76b0e1f2-3ee7-44ae-80af-2fe6ff7f0b73", - "role_id": "dd6bad92-8ff0-4928-93ab-b356c75f2a81" - }, - { - "id": "059ab35e-c204-4c5a-acc3-4119662777ae", - "created_date": "2024-08-16 23:59:54.213000", - "last_modified_date": "2024-08-16 23:59:54.213000", - "version": 0, - "user_id": "3f60db8b-f6e4-47b6-9cde-d9f0040d981d", - "role_id": "0e0a5291-e74b-4ef8-823a-103f731e58bf" - }, - { - "id": "48a3422f-ee66-4e97-9404-1a4c130b9651", - "created_date": "2024-08-16 23:59:54.209000", - "last_modified_date": "2024-08-16 23:59:54.209000", - "version": 0, - "user_id": "3f60db8b-f6e4-47b6-9cde-d9f0040d981d", - "role_id": "d975bc73-1c21-4344-a500-4a90440edf7d" - }, - { - "id": "8a66287a-8798-4031-9cb2-dfefe0784aaa", - "created_date": "2024-08-16 23:58:34.679000", - "last_modified_date": "2024-08-16 23:58:34.679000", - "version": 0, - "user_id": "76b0e1f2-3ee7-44ae-80af-2fe6ff7f0b73", - "role_id": "0e0a5291-e74b-4ef8-823a-103f731e58bf" - }, - { - "id": "af3bcd8b-f680-4cd0-b0f0-d9fcd11cc4f5", - "created_date": "2024-08-16 23:59:54.216000", - "last_modified_date": "2024-08-16 23:59:54.216000", - "version": 0, - "user_id": "3f60db8b-f6e4-47b6-9cde-d9f0040d981d", - "role_id": "dd6bad92-8ff0-4928-93ab-b356c75f2a81" - }, - { - "id": "d2b19e65-a129-426e-b7e3-c9bf2f0a6542", - "created_date": "2024-10-23 17:23:46.625000", - "last_modified_date": "2024-10-23 17:23:46.625000", - "version": 0, - "user_id": "3f60db8b-f6e4-47b6-9cde-d9f0040d981d", - "role_id": "05a186f6-36a2-4cce-8904-187301193937" - }, - { - "id": "e408ff0f-33eb-4151-bb75-482452fc6b35", - "created_date": "2024-08-16 23:59:58.569000", - "last_modified_date": "2024-08-16 23:59:58.569000", - "version": 0, - "user_id": "76b0e1f2-3ee7-44ae-80af-2fe6ff7f0b73", - "role_id": "d975bc73-1c21-4344-a500-4a90440edf7d" - } - ], - "vendor": [ - { - "id": "035fb6c7-c702-400f-97bd-b98ad12963bb", - "created_date": "2024-08-16 23:58:37.457000", - "last_modified_date": "2024-08-16 23:58:37.457000", - "version": 0, - "name": "Donruss" - }, - { - "id": "18f1263f-ef9a-42d0-9391-1ead13b16880", - "created_date": "2024-08-16 23:58:37.456000", - "last_modified_date": "2024-08-16 23:58:37.456000", - "version": 0, - "name": "Topps" - }, - { - "id": "220d410d-75a2-4b33-94cf-b9fca23666eb", - "created_date": "2024-08-16 23:58:37.460000", - "last_modified_date": "2024-08-16 23:58:37.460000", - "version": 0, - "name": "Flair" - }, - { - "id": "5d409413-db78-43b3-bc7a-a76f5718c433", - "created_date": "2024-08-16 23:58:37.445000", - "last_modified_date": "2024-08-16 23:58:37.445000", - "version": 0, - "name": "Pacific" - }, - { - "id": "5e0f3f5d-c58b-4517-b30b-e767c34404ab", - "created_date": "2024-08-16 23:58:37.452000", - "last_modified_date": "2024-08-16 23:58:37.452000", - "version": 0, - "name": "Leaf" - }, - { - "id": "6472c0a1-2b4f-440f-813d-66ea3540f78c", - "created_date": "2024-08-16 23:58:37.454000", - "last_modified_date": "2024-08-16 23:58:37.454000", - "version": 0, - "name": "Upper Deck" - }, - { - "id": "d7617bf3-056a-489b-8563-628f2dd3da84", - "created_date": "2024-08-16 23:58:37.448000", - "last_modified_date": "2024-08-16 23:58:37.448000", - "version": 0, - "name": "Fleer" - }, - { - "id": "d9d8f727-7375-490a-8679-26c834b890a0", - "created_date": "2024-08-16 23:58:37.459000", - "last_modified_date": "2024-08-16 23:58:37.459000", - "version": 0, - "name": "Score" - }, - { - "id": "eb42fc0c-20ad-4e5b-b99a-552ac861db05", - "created_date": "2024-08-16 23:58:37.450000", - "last_modified_date": "2024-08-16 23:58:37.450000", - "version": 0, - "name": "Bowman" - } - ], - "rooster": [ - { - "id": "0ff455c2-8962-4d9d-aedd-2909b251e17d", - "created_date": "2024-08-16 23:58:37.569000", - "last_modified_date": "2024-08-16 23:58:37.569000", - "version": 0, - "year": 2001, - "player_id": "02fc65f6-7d12-41c1-936d-e5dd20da58ba", - "position_id": "d113f005-7792-494d-ad98-b1ce9a30130c", - "team_id": "3fec6af3-d87d-4dd5-b15c-1c6982135920" - }, - { - "id": "3f64b27d-893a-4c70-997e-a5d820a9aa45", - "created_date": "2024-08-16 23:58:37.549000", - "last_modified_date": "2024-08-16 23:58:37.549000", - "version": 0, - "year": 2001, - "player_id": "d6b1a09d-3984-41cb-8b0a-d37d77e2c8ff", - "position_id": "3682a19c-52de-40d5-9307-126d4c579d69", - "team_id": "88fb6005-0ec2-4e5e-8f78-8c2c172c0ce5" - }, - { - "id": "437bab97-1abf-4b41-b118-3c607dfd635d", - "created_date": "2024-08-16 23:58:37.559000", - "last_modified_date": "2024-08-16 23:58:37.559000", - "version": 0, - "year": 2001, - "player_id": "887504f8-5038-4335-90ba-487ac139155c", - "position_id": "0a2976b2-a6f5-4e87-86e0-97270f64958d", - "team_id": "0c1d90c1-a0cb-4f58-94e6-58a419e4d8b8" - }, - { - "id": "593e70f9-fcea-428d-8dbd-a4d4909e54ad", - "created_date": "2024-08-16 23:58:37.554000", - "last_modified_date": "2024-08-16 23:58:37.554000", - "version": 0, - "year": 2001, - "player_id": "a189b76f-9390-49f1-b5ab-47b35d00cb7f", - "position_id": "cd7e15d3-2d95-415b-9dfe-18ca3ed53a26", - "team_id": "934264a3-0333-44f3-a64a-8cb4b9a8e81e" - }, - { - "id": "8842ec4c-fddd-46ea-9709-a35c78441b07", - "created_date": "2024-08-16 23:58:37.565000", - "last_modified_date": "2024-08-16 23:58:37.565000", - "version": 0, - "year": 2001, - "player_id": "e83cddec-19b0-4d35-929c-8acd7a7b7cbf", - "position_id": "e4201d7e-fe49-439e-ad2e-e95f27f05ed5", - "team_id": "1cdf0d19-2ed8-471c-b689-cca82c557a5d" - }, - { - "id": "ad4d73ec-97c2-4f6e-8e31-fff4e38985de", - "created_date": "2024-08-16 23:58:37.562000", - "last_modified_date": "2024-08-16 23:58:37.562000", - "version": 0, - "year": 2001, - "player_id": "5ce64865-db01-4c55-9bb1-72ea1bea9b08", - "position_id": "3682a19c-52de-40d5-9307-126d4c579d69", - "team_id": "1cdf0d19-2ed8-471c-b689-cca82c557a5d" - }, - { - "id": "b773e310-61dd-4a9c-8478-e80d1db1faaf", - "created_date": "2024-08-16 23:58:37.557000", - "last_modified_date": "2024-08-16 23:58:37.557000", - "version": 0, - "year": 2001, - "player_id": "5ee26469-7618-45ef-ba98-f83c3931f2d3", - "position_id": "e4201d7e-fe49-439e-ad2e-e95f27f05ed5", - "team_id": "0c1d90c1-a0cb-4f58-94e6-58a419e4d8b8" - }, - { - "id": "c294649f-f36b-4ba2-a695-6329820332fa", - "created_date": "2024-08-16 23:58:37.571000", - "last_modified_date": "2024-08-16 23:58:37.571000", - "version": 0, - "year": 2001, - "player_id": "983cc139-f2a2-46cd-9fdc-3ec1abf0d5a5", - "position_id": "e4201d7e-fe49-439e-ad2e-e95f27f05ed5", - "team_id": "3fec6af3-d87d-4dd5-b15c-1c6982135920" - }, - { - "id": "cba79538-b728-42b4-9e85-91fc3609f194", - "created_date": "2024-08-16 23:58:37.566000", - "last_modified_date": "2024-08-16 23:58:37.566000", - "version": 0, - "year": 2002, - "player_id": "e83cddec-19b0-4d35-929c-8acd7a7b7cbf", - "position_id": "e4201d7e-fe49-439e-ad2e-e95f27f05ed5", - "team_id": "179f15e0-6628-40a0-b79d-30fffa5445b2" - }, - { - "id": "da266d28-01c9-4f5f-a166-d984743fa9a5", - "created_date": "2024-08-16 23:58:37.560000", - "last_modified_date": "2024-08-16 23:58:37.560000", - "version": 0, - "year": 2001, - "player_id": "5038dd4f-203f-4cb7-99dd-c12c7da4735f", - "position_id": "d113f005-7792-494d-ad98-b1ce9a30130c", - "team_id": "1cdf0d19-2ed8-471c-b689-cca82c557a5d" - }, - { - "id": "f7d50eb8-b42f-4240-8bd6-2321f0d94d93", - "created_date": "2024-08-16 23:58:37.568000", - "last_modified_date": "2024-08-16 23:58:37.568000", - "version": 0, - "year": 2001, - "player_id": "cd4e3f5f-1e03-4d9b-973a-e8f9b7892dd8", - "position_id": "54aedda1-1732-4f14-98f4-0aecc924df15", - "team_id": "3fec6af3-d87d-4dd5-b15c-1c6982135920" - } - ], - "media_article": [], - "user": [ - { - "id": "3f60db8b-f6e4-47b6-9cde-d9f0040d981d", - "created_date": "2024-08-16 23:59:54.185000", - "last_modified_date": "2024-08-16 23:59:54.185000", - "version": 0, - "first_name": "Thomas", - "last_name": "Peetz", - "user_name": "tpeetz", - "email": "thomas.peetz@thpeetz.de", - "password": "$2a$11$6QaMld8u3ONEKuCKLDCzqua4/CL8MBgskq4TaKb/BW2tFs8VsOy6S", - "enabled": 1 - }, - { - "id": "76b0e1f2-3ee7-44ae-80af-2fe6ff7f0b73", - "created_date": "2024-08-16 23:58:34.642000", - "last_modified_date": "2024-08-16 23:58:34.642000", - "version": 0, - "first_name": "Admin", - "last_name": "Administrator", - "user_name": "admin", - "email": "admin@example.org", - "password": "$2a$11$ghUnLGowsDn8wr67fGTJ4OxdEI3H7yL8Do5cR4/E.jhNmHg7TO8Jy", - "enabled": 0 - } - ], - "article": [], - "card": [ - { - "id": "0bf12b65-9739-4418-a792-47f4a97077d5", - "created_date": "2024-08-16 23:58:37.594000", - "last_modified_date": "2024-08-16 23:58:37.594000", - "version": 0, - "card_number": 103, - "year": 2001, - "card_set_id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", - "rooster_id": "b773e310-61dd-4a9c-8478-e80d1db1faaf", - "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" - }, - { - "id": "18935b6e-744b-4297-9840-9a4a71543997", - "created_date": "2024-08-16 23:58:37.596000", - "last_modified_date": "2024-08-16 23:58:37.596000", - "version": 0, - "card_number": 112, - "year": 2001, - "card_set_id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", - "rooster_id": "437bab97-1abf-4b41-b118-3c607dfd635d", - "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" - }, - { - "id": "239e1b5c-38f9-41e1-9798-fe365e878850", - "created_date": "2024-08-16 23:58:37.604000", - "last_modified_date": "2024-08-16 23:58:37.604000", - "version": 0, - "card_number": 335, - "year": 2001, - "card_set_id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", - "rooster_id": "0ff455c2-8962-4d9d-aedd-2909b251e17d", - "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" - }, - { - "id": "47432779-9460-4c09-9823-8520feb3b63b", - "created_date": "2024-08-16 23:58:37.588000", - "last_modified_date": "2024-08-16 23:58:37.588000", - "version": 0, - "card_number": 185, - "year": 2001, - "card_set_id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", - "rooster_id": "3f64b27d-893a-4c70-997e-a5d820a9aa45", - "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" - }, - { - "id": "8bcf6a19-596f-4be3-bfbe-0642e78122b5", - "created_date": "2024-08-16 23:58:37.603000", - "last_modified_date": "2024-08-16 23:58:37.603000", - "version": 0, - "card_number": 338, - "year": 2001, - "card_set_id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", - "rooster_id": "f7d50eb8-b42f-4240-8bd6-2321f0d94d93", - "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" - }, - { - "id": "9c06d0a4-2091-4406-a1cc-dda3da79ced0", - "created_date": "2024-08-16 23:58:37.600000", - "last_modified_date": "2024-08-16 23:58:37.600000", - "version": 0, - "card_number": 31, - "year": 2001, - "card_set_id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", - "rooster_id": "8842ec4c-fddd-46ea-9709-a35c78441b07", - "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" - }, - { - "id": "a29beea1-3e4c-4dc7-b131-686b0af11496", - "created_date": "2024-08-16 23:58:37.592000", - "last_modified_date": "2024-08-16 23:58:37.592000", - "version": 0, - "card_number": 250, - "year": 2001, - "card_set_id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", - "rooster_id": "593e70f9-fcea-428d-8dbd-a4d4909e54ad", - "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" - }, - { - "id": "a94a9d19-9d46-403b-9561-7341ad2d2b52", - "created_date": "2024-08-16 23:58:37.597000", - "last_modified_date": "2024-08-16 23:58:37.597000", - "version": 0, - "card_number": 37, - "year": 2001, - "card_set_id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", - "rooster_id": "da266d28-01c9-4f5f-a166-d984743fa9a5", - "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" - }, - { - "id": "ac7cbab9-fe01-4cec-9b9f-4f77541d165d", - "created_date": "2024-08-16 23:58:37.599000", - "last_modified_date": "2024-08-16 23:58:37.599000", - "version": 0, - "card_number": 38, - "year": 2001, - "card_set_id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", - "rooster_id": "ad4d73ec-97c2-4f6e-8e31-fff4e38985de", - "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" - }, - { - "id": "f4c2ebca-5bfd-41cb-b8cd-71dee25c5e96", - "created_date": "2024-08-16 23:58:37.606000", - "last_modified_date": "2024-08-16 23:58:37.606000", - "version": 0, - "card_number": 345, - "year": 2001, - "card_set_id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", - "rooster_id": "c294649f-f36b-4ba2-a695-6329820332fa", - "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" - } - ], - "media_file": [ - { - "id": "00078197-3ef4-4fdf-9eeb-f4714f61c09a", - "created_date": "2024-07-25 07:29:47.548460", - "last_modified_date": "2024-11-12 22:55:14.432000", - "version": 8, - "url": "https://ge.xhamster.com/videos/family-strokes-busty-stepsis-sneaks-into-her-stepbros-room-for-a-quickie-after-passionate-shower-xhqxWO9", - "review": 0, - "should_download": 0, - "title": "Family Strokes - die vollbusige Stiefschwester schleicht sich nach einem leidenschaftlichen Duschen in das Zimmer ihres Stiefbruders | xHamster", - "file_name": "Family Strokes - die vollbusige Stiefschwester schleicht sich nach einem leidenschaftlichen Duschen in das Zimmer ihres Stiefbruders [xhqxWO9].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/00078197-3ef4-4fdf-9eeb-f4714f61c09a.mp4" - }, - { - "id": "0014d94a-6846-4abf-9fc6-2e271dbf59ad", - "created_date": "2024-07-25 07:29:45.915753", - "last_modified_date": "2024-07-25 07:29:45.915753", - "version": 0, - "url": "https://ge.xhamster.com/videos/doppelte-lust-full-german-movie-xhDRBCS", - "review": 0, - "should_download": 0, - "title": "Doppelte Lust- Full German Movie, Free Porn 39 | xHamster", - "file_name": "Doppelte Lust- full german movie [xhDRBCS].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0014d94a-6846-4abf-9fc6-2e271dbf59ad.mp4" - }, - { - "id": "0097749b-c37f-4a6d-81fe-a1906bda6b97", - "created_date": "2024-07-25 07:29:46.407645", - "last_modified_date": "2024-07-25 07:29:46.407645", - "version": 0, - "url": "https://ge.xhamster.com/videos/three-sisters-vintage-movie-clip-14390892", - "review": 0, - "should_download": 0, - "title": "Drei Schwestern (alter Filmclip) | xHamster", - "file_name": "Drei Schwestern (alter Filmclip) [14390892].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0097749b-c37f-4a6d-81fe-a1906bda6b97.mp4" - }, - { - "id": "00b13a80-cbdb-4080-962e-d6d6455f6785", - "created_date": "2024-07-25 07:29:44.997284", - "last_modified_date": "2024-07-25 07:29:44.997284", - "version": 0, - "url": "https://ge.xhamster.com/videos/endlich-sommer-endlich-anal-im-hohen-grass-xhAW2N0", - "review": 0, - "should_download": 0, - "title": "Endlich Sommer Endlich Anal Im Hohen Grass: Free HD Porn e5 | xHamster", - "file_name": "Endlich Sommer, endlich Anal im hohen Grass [xhAW2N0].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/00b13a80-cbdb-4080-962e-d6d6455f6785.mp4" - }, - { - "id": "00b9fe81-01d6-4cd8-a5db-080a835e341e", - "created_date": "2024-07-25 07:29:47.918268", - "last_modified_date": "2024-07-25 07:29:47.918268", - "version": 0, - "url": "https://ge.xhamster.com/videos/agent-aika-1-7900892", - "review": 0, - "should_download": 0, - "title": "Agent Aika 1: Free 18 Year Old Porn Video 59 | xHamster", - "file_name": "agent aika 1 [7900892].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/00b9fe81-01d6-4cd8-a5db-080a835e341e.mp4" - }, - { - "id": "00e48b0a-1f2b-462a-b20d-10c5c7d9dcbd", - "created_date": "2024-07-25 07:29:47.770607", - "last_modified_date": "2024-12-30 23:01:48.271000", - "version": 1, - "url": "https://ge.xhamster.com/videos/your-the-best-big-stepbrother-ever-madi-collins-gushes-to-her-stepbro-s22-e2-xhb49W9", - "review": 0, - "should_download": 0, - "title": "\"Du bist der beste gro\u00dfe Stiefbruder aller Zeiten!\" Madi Collins sprudelt vor ihrem Stiefbruder22: e2 | xHamster", - "file_name": "\uff02Du bist der beste gro\u00dfe Stiefbruder aller Zeiten!\uff02 Madi Collins sprudelt vor ihrem Stiefbruder22\uff1a e2 [xhb49W9].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/00e48b0a-1f2b-462a-b20d-10c5c7d9dcbd.mp4" - }, - { - "id": "0146bc3c-aa27-4eb9-872f-3af8b46c90ec", - "created_date": "2024-07-25 07:29:44.993784", - "last_modified_date": "2024-10-09 12:40:02.438000", - "version": 1, - "url": "https://ge.xhamster.com/videos/office-girls-full-movie-xhxJtYW", - "review": 0, - "should_download": 0, - "title": "B\u00fcro-M\u00e4dchen (kompletter Film) | xHamster", - "file_name": "7fd7b16a-382f-4ed6-ac1d-bdf08855641b.mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0146bc3c-aa27-4eb9-872f-3af8b46c90ec.mp4" - }, - { - "id": "0174c9b5-f4f1-42a9-997b-1ad85e42f36f", - "created_date": "2024-09-05 20:03:45.856643", - "last_modified_date": "2024-09-25 06:04:58.294000", - "version": 1, - "url": "https://ge.xhamster.com/videos/inzt-insel-xhn7u3d", - "review": 0, - "should_download": 0, - "title": "Inzt-insel: Free Threesome Porn Video 86 | xHamster", - "file_name": "Inzt-Insel [xhn7u3d].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/0174c9b5-f4f1-42a9-997b-1ad85e42f36f.mp4" - }, - { - "id": "017f226c-63b3-42a9-a451-2228809e5603", - "created_date": "2024-07-25 07:29:44.791100", - "last_modified_date": "2024-07-25 07:29:44.791100", - "version": 0, - "url": "https://ge.xhamster.com/videos/er-fickt-seine-mitbewohnerin-xhojScu", - "review": 0, - "should_download": 0, - "title": "Er Fickt Seine Mitbewohnerin, Free Tight Pussy HD Porn 9f | xHamster", - "file_name": "Er fickt seine Mitbewohnerin [xhojScu].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/017f226c-63b3-42a9-a451-2228809e5603.mp4" - }, - { - "id": "018162cc-d900-4a47-b753-467c755d805e", - "created_date": "2024-10-14 20:33:38.255531", - "last_modified_date": "2024-10-21 16:24:41.833000", - "version": 1, - "url": "https://ge.xhamster.com/videos/classic-anal-7276737", - "review": 0, - "should_download": 0, - "title": "Klassischer Analsex | xHamster", - "file_name": "Klassischer Analsex [7276737].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/018162cc-d900-4a47-b753-467c755d805e.mp4" - }, - { - "id": "01dad984-690b-424f-b9d4-7783a5de9c0b", - "created_date": "2024-07-25 07:29:46.953383", - "last_modified_date": "2024-07-25 07:29:46.953383", - "version": 0, - "url": "https://ge.xhamster.com/videos/lets-bang-my-stepmom-s20-e4-xhmJJJO", - "review": 0, - "should_download": 0, - "title": "Lass uns meine stiefmutter knallen - s20: e4 | xHamster", - "file_name": "Lass uns meine stiefmutter knallen - s20\uff1a e4 [xhmJJJO].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/01dad984-690b-424f-b9d4-7783a5de9c0b.mp4" - }, - { - "id": "01edab37-a939-411f-bc70-ea219f66867a", - "created_date": "2024-07-25 07:29:44.643317", - "last_modified_date": "2024-07-25 07:29:44.643317", - "version": 0, - "url": "https://ge.xhamster.com/videos/i-came-so-hard-i-pushed-him-out-of-me-lexi-luna-tells-lana-smalls-s7-e7-xhBJUlE", - "review": 0, - "should_download": 0, - "title": "\"Ich bin so hart gekommen, dass ich ihn aus mir gesto\u00dfen habe\" Lexi Luna sagt zu Lana Smalls - S7: E7 | xHamster", - "file_name": "\uff02Ich bin so hart gekommen, dass ich ihn aus mir gesto\u00dfen habe\uff02 Lexi Luna sagt zu Lana Smalls - S7\uff1a E7 [xhBJUlE].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/01edab37-a939-411f-bc70-ea219f66867a.mp4" - }, - { - "id": "0289b250-8ae0-48be-8ff1-35df94a8c8d3", - "created_date": "2024-07-25 07:29:46.670160", - "last_modified_date": "2024-07-25 07:29:46.670160", - "version": 0, - "url": "https://ge.xhamster.com/videos/appalled-stepmom-catches-her-stepson-touching-himself-while-his-stepsister-dances-on-the-bed-mylf-xhaJe5e", - "review": 0, - "should_download": 0, - "title": "Entsetzte stiefmutter erwischt ihren stiefsohn beim ber\u00fchren, w\u00e4hrend seine stiefschwester auf dem bett tanzt - MYLF | xHamster", - "file_name": "Entsetzte stiefmutter erwischt ihren stiefsohn beim ber\u00fchren, w\u00e4hrend seine stiefschwester auf dem bett tanzt - MYLF [xhaJe5e].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0289b250-8ae0-48be-8ff1-35df94a8c8d3.mp4" - }, - { - "id": "028cde1a-774b-4a2f-b1f6-23305d5fb17b", - "created_date": "2024-08-28 23:21:54.370386", - "last_modified_date": "2024-08-28 23:21:54.370386", - "version": 0, - "url": "https://ge.xhamster.com/videos/summer-camp-girls-1983-9356435", - "review": 0, - "should_download": 0, - "title": "Sommerlager-M\u00e4dchen (1983) | xHamster", - "file_name": "Sommerlager-M\u00e4dchen (1983) [9356435].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/028cde1a-774b-4a2f-b1f6-23305d5fb17b.mp4" - }, - { - "id": "02923b61-2065-46e7-9843-0b8c83d8e268", - "created_date": "2024-07-25 07:29:47.658565", - "last_modified_date": "2024-07-25 07:29:47.658565", - "version": 0, - "url": "https://ge.xhamster.com/videos/wunsche-und-perversionen-1976-aka-desirs-et-perversions-xhP0HAt", - "review": 0, - "should_download": 0, - "title": "Wunsche Und Perversionen 1976 Aka Desirs Et Perversions | xHamster", - "file_name": "Wunsche und Perversionen (1976) aka Desirs et perversions [xhP0HAt].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/02923b61-2065-46e7-9843-0b8c83d8e268.mp4" - }, - { - "id": "02a6b022-60d0-4273-9f0d-d8a11c4874eb", - "created_date": "2025-01-19 13:42:30.070779", - "last_modified_date": "2025-01-19 13:42:30.070785", - "version": 0, - "url": "https://ge.xhamster.com/videos/giada-and-michelle-fuck-on-the-sofa-with-friends-xhK6oKV", - "review": 0, - "should_download": 0, - "title": "Giada und Michelle ficken mit freunden auf dem Sofa | xHamster", - "file_name": "Giada und Michelle ficken mit freunden auf dem Sofa [xhK6oKV].mp4", - "path": null, - "cloud_link": null - }, - { - "id": "03384a21-14ad-4a34-9f35-87f009507018", - "created_date": "2024-07-25 07:29:46.153823", - "last_modified_date": "2024-07-25 07:29:46.153823", - "version": 0, - "url": "https://ge.xhamster.com/videos/hot-blonde-starts-a-hot-orgy-and-gets-fucked-hard-in-all-holes-xh27Wki", - "review": 0, - "should_download": 0, - "title": "hei\u00dfe Blondine startet eine hei\u00dfe Orgie und wird hart gefickt in alle l\u00f6cher | xHamster", - "file_name": "hei\u00dfe Blondine startet eine hei\u00dfe Orgie und wird hart gefickt in alle l\u00f6cher [xh27Wki].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/03384a21-14ad-4a34-9f35-87f009507018.mp4" - }, - { - "id": "03b87a54-ecfa-49a0-800c-5c63f8baa0ed", - "created_date": "2024-07-25 07:29:45.386206", - "last_modified_date": "2024-07-25 07:29:45.386206", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepdaughter-seduced-while-family-camping-with-daddy-xhRgzwd", - "review": 0, - "should_download": 0, - "title": "Stieftochter beim Familiencamping mit Papa verf\u00fchrt | xHamster", - "file_name": "Stieftochter beim Familiencamping mit Papa verf\u00fchrt [xhRgzwd].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/03b87a54-ecfa-49a0-800c-5c63f8baa0ed.mp4" - }, - { - "id": "03c0caa8-5aa3-4624-89e2-ceafc23d78d5", - "created_date": "2024-08-28 23:21:54.358831", - "last_modified_date": "2024-08-28 23:21:54.358831", - "version": 0, - "url": "https://ge.xhamster.com/videos/some-cans-make-cute-girls-feel-lusty-and-take-part-in-hot-saturnalia-xhpFkf3", - "review": 0, - "should_download": 0, - "title": "Einige Dosen lassen s\u00fc\u00dfe M\u00e4dchen lustvoll werden und nehmen an hei\u00dfen Saturnalia teil | xHamster", - "file_name": "Einige Dosen lassen s\u00fc\u00dfe M\u00e4dchen lustvoll werden und nehmen an hei\u00dfen Saturnalia teil [xhpFkf3].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/03c0caa8-5aa3-4624-89e2-ceafc23d78d5.mp4" - }, - { - "id": "03c134e9-4666-4198-b809-dd923b8f84aa", - "created_date": "2024-07-25 07:29:46.829507", - "last_modified_date": "2024-07-25 07:29:46.829507", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-godfather-comes-to-visit-and-i-convince-him-to-get-in-the-pool-until-he-finishes-me-xhGDlup", - "review": 0, - "should_download": 0, - "title": "Mein pate kommt zu besuch und ich \u00fcberrede ihn, in den pool zu kommen, bis er mich fertig macht | xHamster", - "file_name": "Mein pate kommt zu besuch und ich \u00fcberrede ihn, in den pool zu kommen, bis er mich fertig macht [xhGDlup].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/03c134e9-4666-4198-b809-dd923b8f84aa.mp4" - }, - { - "id": "0424a283-580e-439d-a197-aca981d0c17d", - "created_date": "2024-07-25 07:29:47.691177", - "last_modified_date": "2024-07-25 07:29:47.691177", - "version": 0, - "url": "https://ge.xhamster.com/videos/carter-cruise-caught-roommate-piper-perri-masturbating-while-sniffing-dirty-panties-gets-turned-on-xhYQqfd", - "review": 0, - "should_download": 0, - "title": "Carter cruise erwischt mitbewohnerin piper perri beim masturbieren am schn\u00fcffeln am schmutzigen h\u00f6schen und wird angemacht | xHamster", - "file_name": "Carter cruise erwischt mitbewohnerin piper perri beim masturbieren am schn\u00fcffeln am schmutzigen h\u00f6schen und wird angemacht [xhYQqfd].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0424a283-580e-439d-a197-aca981d0c17d.mp4" - }, - { - "id": "044bd91b-3898-4f28-b24a-956c6a7085cb", - "created_date": "2024-07-25 07:29:47.163565", - "last_modified_date": "2024-07-25 07:29:47.163565", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepmom-says-go-ahead-put-it-in-xheBKuW", - "review": 0, - "should_download": 0, - "title": "Stiefmutter sagt, mach weiter! Steck ihn rein! | xHamster", - "file_name": "Stiefmutter sagt, mach weiter! Steck ihn rein! [xheBKuW].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/044bd91b-3898-4f28-b24a-956c6a7085cb.mp4" - }, - { - "id": "044d0b60-8789-43ff-b805-8a443a459982", - "created_date": "2024-07-25 07:29:46.802774", - "last_modified_date": "2024-07-25 07:29:46.802774", - "version": 0, - "url": "https://ge.xhamster.com/videos/biologiestunde-wird-zum-gangbang-xhKxLBY", - "review": 0, - "should_download": 0, - "title": "Biologiestunde Wird Zum Gangbang, Free Porn 7b | xHamster", - "file_name": "Biologiestunde wird zum Gangbang [xhKxLBY].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/044d0b60-8789-43ff-b805-8a443a459982.mp4" - }, - { - "id": "044feea9-781e-4644-9da1-6b6c75984d87", - "created_date": "2024-07-25 07:29:45.616760", - "last_modified_date": "2024-07-25 07:29:45.616760", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsis-says-are-you-touching-yourself-and-staring-at-me-xhTGVR5", - "review": 0, - "should_download": 0, - "title": "Stiefschwester sagt, ber\u00fchrst du dich selbst und starrst mich an ?! | xHamster", - "file_name": "Stiefschwester sagt, ber\u00fchrst du dich selbst und starrst mich an \uff1f! [xhTGVR5].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/044feea9-781e-4644-9da1-6b6c75984d87.mp4" - }, - { - "id": "0465fa7a-44bf-41d7-b1d4-d01d0fab813f", - "created_date": "2024-07-25 07:29:47.734387", - "last_modified_date": "2024-07-25 07:29:47.734387", - "version": 0, - "url": "https://ge.xhamster.com/videos/2-guys-and-3-girls-playing-some-strip-games-14066218", - "review": 0, - "should_download": 0, - "title": "2 Typen und 3 M\u00e4dchen spielen Strip-Spiele | xHamster", - "file_name": "2 Typen und 3 M\u00e4dchen spielen Strip-Spiele [14066218].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0465fa7a-44bf-41d7-b1d4-d01d0fab813f.mp4" - }, - { - "id": "047f8800-aa71-4177-8f6b-bdde9bdf5516", - "created_date": "2024-08-09 21:31:03.811829", - "last_modified_date": "2024-09-06 09:41:50.230000", - "version": 3, - "url": "https://ge.xhamster.com/videos/first-love-episode-1-xhGLIaq", - "review": 0, - "should_download": 0, - "title": "Video in \u00dcberpr\u00fcfung", - "file_name": "Erste liebe episode 1 [xhGLIaq].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/047f8800-aa71-4177-8f6b-bdde9bdf5516.mp4" - }, - { - "id": "048855b3-bf66-4913-a963-1f39ada62cc2", - "created_date": "2024-07-25 07:29:44.699379", - "last_modified_date": "2024-07-25 07:29:44.699379", - "version": 0, - "url": "https://ge.xhamster.com/videos/flashing-my-pussy-in-front-of-a-man-in-public-beach-and-he-helps-me-squirt-its-very-risky-misscreamy-xhqH4q3", - "review": 0, - "should_download": 0, - "title": "Ich zeige meine Muschi vor einem Mann am \u00f6ffentlichen Strand und er hilft mir beim Spritzen \u2013 das ist sehr riskant \u2013 MissCreamy | xHamster", - "file_name": "Ich zeige meine Muschi vor einem Mann am \u00f6ffentlichen Strand und er hilft mir beim Spritzen \u2013 das ist sehr riskant \u2013 MissCreamy [xhqH4q3].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/048855b3-bf66-4913-a963-1f39ada62cc2.mp4" - }, - { - "id": "04e75137-e86d-4fa3-be27-b0c0bcbbf6e3", - "created_date": "2024-11-01 21:08:26.930541", - "last_modified_date": "2024-11-01 21:08:26.930541", - "version": 0, - "url": "https://ge.xhamster.com/videos/best-of-1159-11057365", - "review": 0, - "should_download": 0, - "title": "Das Beste von # 1159 | xHamster", - "file_name": "Das Beste von # 1159 [11057365].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/04e75137-e86d-4fa3-be27-b0c0bcbbf6e3.mp4" - }, - { - "id": "04ee7dd7-cf7a-445e-ab0b-bb74298191da", - "created_date": "2024-07-25 07:29:46.370333", - "last_modified_date": "2024-07-25 07:29:46.370333", - "version": 0, - "url": "https://ge.xhamster.com/videos/heidi-spritzbuben-der-berge-teil-5-8129114", - "review": 0, - "should_download": 0, - "title": "Heidi Spritzbuben Der Berge Teil 5, Free Porn d0 | xHamster", - "file_name": "Heidi Spritzbuben Der Berge Teil 5 [8129114].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/04ee7dd7-cf7a-445e-ab0b-bb74298191da.mp4" - }, - { - "id": "05075485-bcf6-46a3-8b68-084b3c7ab55c", - "created_date": "2024-07-25 07:29:46.795435", - "last_modified_date": "2024-07-25 07:29:46.795435", - "version": 0, - "url": "https://ge.xhamster.com/videos/mama-rewards-two-boys-hard-work-with-hot-dp-anal-action-2041440", - "review": 0, - "should_download": 0, - "title": "Mama belohnt zwei Jungs harte Arbeit mit hei\u00dfer Doppelpenetration !! | xHamster", - "file_name": "Mama belohnt zwei Jungs harte Arbeit mit hei\u00dfer Doppelpenetration !! [2041440].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/05075485-bcf6-46a3-8b68-084b3c7ab55c.mp4" - }, - { - "id": "051d862c-6eef-4f67-972c-6221174e1609", - "created_date": "2024-10-07 20:47:56.417683", - "last_modified_date": "2024-10-07 20:47:56.417683", - "version": 0, - "url": "https://ge.xhamster.com/videos/public-beach-sex-in-spain-everyone-can-finger-and-fuck-me-on-the-beach-xhSydAg", - "review": 0, - "should_download": 0, - "title": "Jeder darf mich am Strand fingern und ficken | xHamster", - "file_name": "Jeder darf mich am Strand fingern und ficken [xhSydAg].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/051d862c-6eef-4f67-972c-6221174e1609.mp4" - }, - { - "id": "05221b06-fd7b-4159-90de-083e546b5816", - "created_date": "2024-07-25 07:29:47.555619", - "last_modified_date": "2024-07-25 07:29:47.555619", - "version": 0, - "url": "https://ge.xhamster.com/videos/do-you-know-how-to-use-your-dick-better-than-a-tennis-racket-milf-cory-chase-asks-stepson-s19-e9-xhNyi3I", - "review": 0, - "should_download": 0, - "title": "\"Wei\u00dft du, wie man deinen schwanz besser benutzt als ein Tennisschl\u00e4ger?\" Milf cory chase fragt stiefsohn - S19: E9 | xHamster", - "file_name": "\uff02Wei\u00dft du, wie man deinen schwanz besser benutzt als ein Tennisschl\u00e4ger\uff1f\uff02 Milf cory chase fragt stiefsohn - S19\uff1a E9 [xhNyi3I].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/05221b06-fd7b-4159-90de-083e546b5816.mp4" - }, - { - "id": "05253103-25ff-4b2e-aab3-d7d2c5ec2b73", - "created_date": "2024-07-25 07:29:46.196509", - "last_modified_date": "2024-07-25 07:29:46.196509", - "version": 0, - "url": "https://ge.xhamster.com/videos/cream-pie-orgies-fuck-groups-threesomes-hardcore-gangbangs-13477993", - "review": 0, - "should_download": 0, - "title": "Creampie-Orgien, ficken, Hardcore-Gangbangs, Dreier | xHamster", - "file_name": "Creampie-Orgien, ficken, Hardcore-Gangbangs, Dreier [13477993].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/05253103-25ff-4b2e-aab3-d7d2c5ec2b73.mp4" - }, - { - "id": "059c5f52-3db4-4a2c-bab4-ac3de1b8d310", - "created_date": "2024-07-25 07:29:46.568972", - "last_modified_date": "2024-07-25 07:29:46.568972", - "version": 0, - "url": "https://ge.xhamster.com/videos/pfadfinderrinnen-12311349", - "review": 0, - "should_download": 0, - "title": "Pfadfinderrinnen: Free European Porn Video 45 | xHamster", - "file_name": "Pfadfinderrinnen [12311349].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/059c5f52-3db4-4a2c-bab4-ac3de1b8d310.mp4" - }, - { - "id": "05ceaeb9-5e2d-4c86-8146-135ebbd56960", - "created_date": "2024-07-25 07:29:46.806524", - "last_modified_date": "2024-07-25 07:29:46.806524", - "version": 0, - "url": "https://ge.xhamster.com/videos/schulmadchen-report-11-1976-5777639", - "review": 0, - "should_download": 0, - "title": "Schulmadchen-Bericht 11 (1976) | xHamster", - "file_name": "Schulmadchen-Bericht 11 (1976) [5777639].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/05ceaeb9-5e2d-4c86-8146-135ebbd56960.mp4" - }, - { - "id": "060ca759-b628-41f2-a5b5-1f40714bced8", - "created_date": "2024-12-29 23:53:27.912424", - "last_modified_date": "2024-12-29 23:53:27.912424", - "version": 0, - "url": "https://ge.xhamster.com/videos/sex-internet-xhjFMjI", - "review": 0, - "should_download": 0, - "title": "Sex & Internet | xHamster", - "file_name": "Sex & Internet [xhjFMjI].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/060ca759-b628-41f2-a5b5-1f40714bced8.mp4" - }, - { - "id": "0659eb79-4a30-4f1b-bfe5-78e99ba9a1af", - "created_date": "2024-07-25 07:29:45.252618", - "last_modified_date": "2024-07-25 07:29:45.252618", - "version": 0, - "url": "https://ge.xhamster.com/videos/full-fun-sex-xhzemUc", - "review": 0, - "should_download": 0, - "title": "Voller Spa\u00df-Sex | xHamster", - "file_name": "Voller Spa\u00df-Sex [xhzemUc].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0659eb79-4a30-4f1b-bfe5-78e99ba9a1af.mp4" - }, - { - "id": "06b582ed-4910-474a-bd27-87495dc5fe4f", - "created_date": "2024-07-25 07:29:45.732777", - "last_modified_date": "2024-07-25 07:29:45.732777", - "version": 0, - "url": "https://ge.xhamster.com/videos/mom-loves-dp-and-his-cock-is-not-to-big-for-her-ass-xhGX68d", - "review": 0, - "should_download": 0, - "title": "MUTTER liebt doppelpenetration und sein schwanz ist nicht zu gro\u00df f\u00fcr ihren ARSCH | xHamster", - "file_name": "MUTTER liebt doppelpenetration und sein schwanz ist nicht zu gro\u00df f\u00fcr ihren ARSCH [xhGX68d].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/06b582ed-4910-474a-bd27-87495dc5fe4f.mp4" - }, - { - "id": "06e26b80-9fb1-4727-ab1c-123260f8967f", - "created_date": "2024-07-25 07:29:47.828611", - "last_modified_date": "2024-07-25 07:29:47.828611", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-stepsister-loves-it-when-i-watch-her-in-the-bathroom-annahomemix-xhfj4po", - "review": 0, - "should_download": 0, - "title": "Meine Stiefschwester liebt es, wenn ich sie im Badezimmer beobachte. annahomemix | xHamster", - "file_name": "Meine Stiefschwester liebt es, wenn ich sie im Badezimmer beobachte. annahomemix [xhfj4po].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/06e26b80-9fb1-4727-ab1c-123260f8967f.mp4" - }, - { - "id": "0782eede-780c-442f-8787-74de7bf0af6c", - "created_date": "2024-09-24 08:11:39.008987", - "last_modified_date": "2024-10-21 16:24:48.315000", - "version": 1, - "url": "https://ge.xhamster.com/videos/babes-step-mom-lessons-cozy-by-the-fire-starring-jay-smo-8274351", - "review": 0, - "should_download": 0, - "title": "Babes - Stiefmutter-Unterricht - gem\u00fctlich am Feuer mit Jay Smo | xHamster", - "file_name": "Babes - Stiefmutter-Unterricht - gem\u00fctlich am Feuer mit Jay Smo [8274351].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/0782eede-780c-442f-8787-74de7bf0af6c.mp4" - }, - { - "id": "07ad27c7-4fef-4c0e-8a27-7a2edf59fc49", - "created_date": "2024-08-16 11:12:31.006000", - "last_modified_date": "2024-10-21 16:24:56.197000", - "version": 1, - "url": "https://ge.xhamster.com/videos/okay-you-can-fuck-me-in-the-ass-but-please-be-quiet-so-that-my-stepmother-doesnt-hear-you-xhqLrKI", - "review": 0, - "should_download": 0, - "title": "Okay, Du kannst mich in den arsch ficken, aber bitte sei ruhig, damit meine stiefmutter dich nicht h\u00f6rt! | xHamster", - "file_name": "Okay, Du kannst mich in den arsch ficken, aber bitte sei ruhig, damit meine stiefmutter dich nicht h\u00f6rt! [xhqLrKI].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/07ad27c7-4fef-4c0e-8a27-7a2edf59fc49.mp4" - }, - { - "id": "07b14ed2-d5c7-4f1e-aea6-c8d1b58292ae", - "created_date": "2024-07-25 07:29:44.363850", - "last_modified_date": "2024-07-25 07:29:44.363850", - "version": 0, - "url": "https://ge.xhamster.com/videos/big-breasted-redhead-step-sister-loves-brother-15017208", - "review": 0, - "should_download": 0, - "title": "Vollbusige rothaarige Stiefschwester liebt Bruder | xHamster", - "file_name": "Vollbusige rothaarige Stiefschwester liebt Bruder [15017208].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/07b14ed2-d5c7-4f1e-aea6-c8d1b58292ae.mp4" - }, - { - "id": "07c09354-86dd-4e7d-9c00-30beeb1f7f1a", - "created_date": "2024-07-25 07:29:46.745501", - "last_modified_date": "2024-07-25 07:29:46.745501", - "version": 0, - "url": "https://ge.xhamster.com/videos/super-funny-pantsing-games-xh1m7oX", - "review": 0, - "should_download": 0, - "title": "Super lustige H\u00f6schen-Spiele | xHamster", - "file_name": "Super lustige H\u00f6schen-Spiele [xh1m7oX].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/07c09354-86dd-4e7d-9c00-30beeb1f7f1a.mp4" - }, - { - "id": "07c8a142-32e6-4bc0-9d35-abf73d992f8a", - "created_date": "2024-07-25 07:29:47.360864", - "last_modified_date": "2024-07-25 07:29:47.360864", - "version": 0, - "url": "https://ge.xhamster.com/videos/2-guys-and-not-their-sisters-in-the-jacuzzi-4387561", - "review": 0, - "should_download": 0, - "title": "2 Typen und nicht ihre Schwestern im Whirlpool | xHamster", - "file_name": "2 Typen und nicht ihre Schwestern im Whirlpool [4387561].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/07c8a142-32e6-4bc0-9d35-abf73d992f8a.mp4" - }, - { - "id": "07d5caf8-5cf2-41dc-81f5-f703485ae863", - "created_date": "2024-07-25 07:29:47.070657", - "last_modified_date": "2024-07-25 07:29:47.070657", - "version": 0, - "url": "https://ge.xhamster.com/videos/sneaky-sex-nick-moreno-lana-roy-silent-retreat-13704297", - "review": 0, - "should_download": 0, - "title": "Hinterh\u00e4ltiger Sex - Nick Moreno Lana Roy - stiller R\u00fcckzug | xHamster", - "file_name": "Hinterh\u00e4ltiger Sex - Nick Moreno Lana Roy - stiller R\u00fcckzug [13704297].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/07d5caf8-5cf2-41dc-81f5-f703485ae863.mp4" - }, - { - "id": "082f4f8c-ce2b-46b1-a2bd-7558bfe828c5", - "created_date": "2024-07-25 07:29:46.622404", - "last_modified_date": "2024-07-25 07:29:46.622404", - "version": 0, - "url": "https://ge.xhamster.com/videos/full-swap-with-my-wife-and-her-best-friend-and-her-husband-11882483", - "review": 0, - "should_download": 0, - "title": "Voller Tausch mit meiner Frau und ihrer besten Freundin und ihrem Ehemann | xHamster", - "file_name": "Voller Tausch mit meiner Frau und ihrer besten Freundin und ihrem Ehemann [11882483].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/082f4f8c-ce2b-46b1-a2bd-7558bfe828c5.mp4" - }, - { - "id": "085389d0-720c-4a06-a009-1f062f5c9114", - "created_date": "2024-07-25 07:29:48.139594", - "last_modified_date": "2024-07-25 07:29:48.139594", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-college-friends-have-an-orgy-12765094", - "review": 0, - "should_download": 0, - "title": "Meine College-Freunde haben eine Orgie | xHamster", - "file_name": "Meine College-Freunde haben eine Orgie [12765094].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/085389d0-720c-4a06-a009-1f062f5c9114.mp4" - }, - { - "id": "088c45f9-52a8-4293-8e09-c031745327a4", - "created_date": "2024-07-25 07:29:47.930540", - "last_modified_date": "2024-07-25 07:29:47.930540", - "version": 0, - "url": "https://ge.xhamster.com/videos/american-college-xxx-the-originalin-hd-story-n-20-xhVOWVl", - "review": 0, - "should_download": 0, - "title": "Amerikanisches College xxx !!! - (das Original in HD) - Geschichte n. # 20 | xHamster", - "file_name": "Amerikanisches College xxx !!! - (das Original in HD) - Geschichte n. # 20 [xhVOWVl].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/088c45f9-52a8-4293-8e09-c031745327a4.mp4" - }, - { - "id": "0898b664-fa92-425d-81ee-b924f56ea4c9", - "created_date": "2024-07-25 07:29:46.432582", - "last_modified_date": "2024-07-25 07:29:46.432582", - "version": 0, - "url": "https://ge.xhamster.com/videos/great-meeting-of-the-depraved-xhEahJr", - "review": 0, - "should_download": 0, - "title": "Gro\u00dfes treffen der verdorbenen | xHamster", - "file_name": "Gro\u00dfes treffen der verdorbenen [xhEahJr].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0898b664-fa92-425d-81ee-b924f56ea4c9.mp4" - }, - { - "id": "08adc827-136e-458c-a839-03070911d4ac", - "created_date": "2024-07-25 07:29:45.075574", - "last_modified_date": "2024-09-06 09:40:44.255000", - "version": 1, - "url": "https://ge.xhamster.com/videos/hot-teen-loses-bet-now-she-fucks-her-stepbrothers-threesome-xhGaKnm", - "review": 0, - "should_download": 0, - "title": "Hei\u00dfes Teen verliert Wette, jetzt fickt sie ihre Stiefbr\u00fcder! ", - "file_name": "Hei\u00dfes Teen verliert Wette, jetzt fickt sie ihre Stiefbr\u00fcder! Dreier [xhGaKnm].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/08adc827-136e-458c-a839-03070911d4ac.mp4" - }, - { - "id": "08fea4ec-bb00-4d7b-8114-d8740033e5c9", - "created_date": "2025-01-19 13:42:38.334114", - "last_modified_date": "2025-01-19 13:42:38.334120", - "version": 0, - "url": "https://ge.xhamster.com/videos/shocked-stepsons-catch-their-mylf-stepmothers-acting-like-dirty-perverts-momswap-xhYY3Lc", - "review": 0, - "should_download": 0, - "title": "Schockierte stiefsohn erwischen ihre MYLF-stiefmutter, die sich wie schmutzige perverse benimmt - momSwap | xHamster", - "file_name": "Schockierte stiefsohn erwischen ihre MYLF-stiefmutter, die sich wie schmutzige perverse benimmt - momSwap [xhYY3Lc].mp4", - "path": null, - "cloud_link": null - }, - { - "id": "09081728-1fc7-41ae-a108-c89814b4f722", - "created_date": "2024-11-10 16:53:33.485868", - "last_modified_date": "2024-11-10 16:53:33.485868", - "version": 0, - "url": "https://ge.xhamster.com/videos/hypnotized-in-laws-11169925", - "review": 0, - "should_download": 0, - "title": "Hypnotized In-laws: Free Latina HD Porn Video b5 | xHamster", - "file_name": "hypnotized in-laws [11169925].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/09081728-1fc7-41ae-a108-c89814b4f722.mp4" - }, - { - "id": "0918b266-a28d-448c-95e0-81f75831e1c9", - "created_date": "2024-07-25 07:29:46.542912", - "last_modified_date": "2024-07-25 07:29:46.542912", - "version": 0, - "url": "https://ge.xhamster.com/videos/jade-sin-gets-double-stuffed-by-white-and-black-dicks-in-a-threesome-xhp3k6H", - "review": 0, - "should_download": 0, - "title": "Jade Sin wird in einem Dreier doppelt von wei\u00dfen und schwarzen Schw\u00e4nzen gestopft | xHamster", - "file_name": "Jade Sin wird in einem Dreier doppelt von wei\u00dfen und schwarzen Schw\u00e4nzen gestopft [xhp3k6H].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0918b266-a28d-448c-95e0-81f75831e1c9.mp4" - }, - { - "id": "09827dd9-888d-413f-9082-42c96cfb157b", - "created_date": "2024-07-25 07:29:45.907625", - "last_modified_date": "2024-07-25 07:29:45.907625", - "version": 0, - "url": "https://ge.xhamster.com/videos/summer-camp-sun-bunnies-2003-xhS9UMh", - "review": 0, - "should_download": 0, - "title": "Sommerlager Sonnenhasen (2003) | xHamster", - "file_name": "Sommerlager Sonnenhasen (2003) [xhS9UMh].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/09827dd9-888d-413f-9082-42c96cfb157b.mp4" - }, - { - "id": "0989ac7f-656f-4456-a7ba-d333d4f87268", - "created_date": "2024-07-25 07:29:44.428513", - "last_modified_date": "2024-07-25 07:29:44.428513", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-kings-servants-6234783", - "review": 0, - "should_download": 0, - "title": "Die Diener der K\u00f6nige | xHamster", - "file_name": "Die Diener der K\u00f6nige [6234783].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0989ac7f-656f-4456-a7ba-d333d4f87268.mp4" - }, - { - "id": "09bd3e92-73dc-46dd-97cf-36de79c7520c", - "created_date": "2024-07-25 07:29:44.655312", - "last_modified_date": "2024-07-25 07:29:44.655312", - "version": 0, - "url": "https://ge.xhamster.com/videos/verschollen-full-movie-xhjxenK", - "review": 0, - "should_download": 0, - "title": "Verschollen Full Movie, Free Story Porn Video 31 | xHamster", - "file_name": "Verschollen (Full Movie) [xhjxenK].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/09bd3e92-73dc-46dd-97cf-36de79c7520c.mp4" - }, - { - "id": "09c2c426-322d-4d2a-aa9c-07dc088dbda9", - "created_date": "2024-10-07 20:47:56.412412", - "last_modified_date": "2024-10-21 16:25:02.666000", - "version": 1, - "url": "https://ge.xhamster.com/videos/threesome-on-couch-4447753", - "review": 0, - "should_download": 0, - "title": "Dreier auf der Couch | xHamster", - "file_name": "Dreier auf der Couch [4447753].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/09c2c426-322d-4d2a-aa9c-07dc088dbda9.mp4" - }, - { - "id": "0a238e83-e2fc-40ab-aa8e-3a80b66e3e88", - "created_date": "2024-07-25 07:29:45.060515", - "last_modified_date": "2024-07-25 07:29:45.060515", - "version": 0, - "url": "https://ge.xhamster.com/videos/die-wette-1009249", - "review": 0, - "should_download": 0, - "title": "Die Wette: Free Anal & Amateur Porn Video e8 | xHamster", - "file_name": "Die Wette ! [1009249].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0a238e83-e2fc-40ab-aa8e-3a80b66e3e88.mp4" - }, - { - "id": "0a2f9d22-71fe-4551-bd40-c7850155ac2c", - "created_date": "2024-07-25 07:29:44.409917", - "last_modified_date": "2024-07-25 07:29:44.409917", - "version": 0, - "url": "https://ge.xhamster.com/videos/18-and-confused-7-xha5g9P", - "review": 0, - "should_download": 0, - "title": "18 und verwirrt # 7 | xHamster", - "file_name": "18 und verwirrt # 7 [xha5g9P].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0a2f9d22-71fe-4551-bd40-c7850155ac2c.mp4" - }, - { - "id": "0a305c25-9c39-45c1-a52c-7ed8e7bbd235", - "created_date": "2024-11-10 16:53:33.484084", - "last_modified_date": "2024-11-10 16:53:33.484084", - "version": 0, - "url": "https://ge.xhamster.com/videos/foursome-between-friends-xhxjxiO", - "review": 0, - "should_download": 0, - "title": "Vierer zwischen freunden | xHamster", - "file_name": "Vierer zwischen freunden [xhxjxiO].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/0a305c25-9c39-45c1-a52c-7ed8e7bbd235.mp4" - }, - { - "id": "0a43e867-a723-4e9e-b832-12a9116b18c5", - "created_date": "2024-08-28 23:21:54.379033", - "last_modified_date": "2024-08-28 23:21:54.379033", - "version": 0, - "url": "https://ge.xhamster.com/videos/what-are-neighbors-for-s18-e7-xhreDis", - "review": 0, - "should_download": 0, - "title": "Wof\u00fcr sind Nachbarn - S18: E7 | xHamster", - "file_name": "Wof\u00fcr sind Nachbarn - S18\uff1a E7 [xhreDis].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/0a43e867-a723-4e9e-b832-12a9116b18c5.mp4" - }, - { - "id": "0a6b7244-f1e5-4e1f-b513-5452ef6e521f", - "created_date": "2024-07-25 07:29:47.145407", - "last_modified_date": "2024-07-25 07:29:47.145407", - "version": 0, - "url": "https://ge.xhamster.com/videos/lovely-girlfriends-spend-the-sunny-day-sucking-and-fucking-xhQwgwY", - "review": 0, - "should_download": 0, - "title": "Sch\u00f6ne Freundinnen verbringen den sonnigen Tag mit Lutschen und Ficken | xHamster", - "file_name": "Sch\u00f6ne Freundinnen verbringen den sonnigen Tag mit Lutschen und Ficken [xhQwgwY].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0a6b7244-f1e5-4e1f-b513-5452ef6e521f.mp4" - }, - { - "id": "0a873200-41c8-41cf-ba62-90cea90fc3d4", - "created_date": "2024-09-24 08:11:38.997987", - "last_modified_date": "2024-10-21 16:25:11.331000", - "version": 1, - "url": "https://ge.xhamster.com/videos/busty-stepmoms-catch-their-respective-stepsons-masturbating-theyre-impressed-by-their-huge-cocks-xhz0hGb", - "review": 0, - "should_download": 0, - "title": "Vollbusige stiefmutter erwischen ihren jeweiligen stiefsohn beim masturbieren und sie sind von ihren riesigen schw\u00e4nzen beeindruckt | xHamster", - "file_name": "Vollbusige stiefmutter erwischen ihren jeweiligen stiefsohn beim masturbieren und sie sind von ihren riesigen schw\u00e4nzen beeindruckt [xhz0hGb].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/0a873200-41c8-41cf-ba62-90cea90fc3d4.mp4" - }, - { - "id": "0b477192-51bc-4af5-8b50-418255f71b4d", - "created_date": "2024-07-25 07:29:47.797765", - "last_modified_date": "2024-07-25 07:29:47.797765", - "version": 0, - "url": "https://ge.xhamster.com/videos/inzt-familie-1-xhhXHZ1", - "review": 0, - "should_download": 0, - "title": "Inzt Familie 1: Free Threesome Porn Video b9 | xHamster", - "file_name": "Inzt.Familie 1 [xhhXHZ1].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0b477192-51bc-4af5-8b50-418255f71b4d.mp4" - }, - { - "id": "0b4e600e-3083-49ae-ad01-9579fde1f7d1", - "created_date": "2024-07-25 07:29:46.861883", - "last_modified_date": "2024-07-25 07:29:46.861883", - "version": 0, - "url": "https://ge.xhamster.com/videos/der-geilen-chef-sekretaerin-auf-die-brille-gespritzt-xhnKQzE", - "review": 0, - "should_download": 0, - "title": "Der Geilen Chef Sekretaerin Auf Die Brille Gespritzt | xHamster", - "file_name": "Der geilen Chef Sekretaerin auf die Brille gespritzt [xhnKQzE].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0b4e600e-3083-49ae-ad01-9579fde1f7d1.mp4" - }, - { - "id": "0b7523f7-fe0f-4e47-be8d-6768ff8b6ae3", - "created_date": "2024-07-25 07:29:46.824900", - "last_modified_date": "2024-07-25 07:29:46.824900", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepbrother-stepsister-fuck-best-friend-family-therapy-14986252", - "review": 0, - "should_download": 0, - "title": "Stiefbruder und Stiefschwester ficken besten Freund - Familientherapie | xHamster", - "file_name": "Stiefbruder und Stiefschwester ficken besten Freund - Familientherapie [14986252].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0b7523f7-fe0f-4e47-be8d-6768ff8b6ae3.mp4" - }, - { - "id": "0bb44610-368e-4624-9170-d11bebed6e46", - "created_date": "2024-07-25 07:29:46.091710", - "last_modified_date": "2024-07-25 07:29:46.091710", - "version": 0, - "url": "https://ge.xhamster.com/videos/take-my-love-full-movie-xhnTUCK", - "review": 0, - "should_download": 0, - "title": "Nimm meine Liebe (kompletter Film) | xHamster", - "file_name": "Nimm meine Liebe (kompletter Film) [xhnTUCK].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0bb44610-368e-4624-9170-d11bebed6e46.mp4" - }, - { - "id": "0c1b1031-5b16-4767-a6a8-4b7734be7ed6", - "created_date": "2024-08-08 01:01:27.749190", - "last_modified_date": "2025-01-03 00:56:29.149000", - "version": 2, - "url": "", - "review": 0, - "should_download": 0, - "title": "", - "file_name": "Enjoying the lake-12246817.mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0c1b1031-5b16-4767-a6a8-4b7734be7ed6.mp4" - }, - { - "id": "0c207e34-c83e-49f0-a9a6-0b248f81c745", - "created_date": "2024-07-25 07:29:45.658141", - "last_modified_date": "2024-07-25 07:29:45.658141", - "version": 0, - "url": "https://ge.xhamster.com/videos/bitch-on-the-beach-stranger-girl-sucked-me-in-public-xh6pEaa", - "review": 0, - "should_download": 0, - "title": "Schlampe am Strand: Fremdes M\u00e4dchen hat mich in der \u00d6ffentlichkeit gelutscht | xHamster", - "file_name": "Schlampe am Strand\uff1a Fremdes M\u00e4dchen hat mich in der \u00d6ffentlichkeit gelutscht [xh6pEaa].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0c207e34-c83e-49f0-a9a6-0b248f81c745.mp4" - }, - { - "id": "0c25d694-970e-40c9-950b-23a4ef93fc81", - "created_date": "2024-07-25 07:29:47.410397", - "last_modified_date": "2024-07-25 07:29:47.410397", - "version": 0, - "url": "https://ge.xhamster.com/videos/dirty-flix-caty-kiss-hot-stepmom-teaches-teen-to-fuck-xhOF1PG", - "review": 0, - "should_download": 0, - "title": "Dirty Flix - Caty Kiss - hei\u00dfe Stiefmutter lehrt Teenager zu ficken | xHamster", - "file_name": "Dirty Flix - Caty Kiss - hei\u00dfe Stiefmutter lehrt Teenager zu ficken [xhOF1PG].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0c25d694-970e-40c9-950b-23a4ef93fc81.mp4" - }, - { - "id": "0c50a81e-4b37-436e-9c08-89ad6b42b6c1", - "created_date": "2024-12-29 23:53:27.920603", - "last_modified_date": "2024-12-29 23:53:27.920603", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepbro-says-how-about-i-make-it-even-and-show-you-my-junk-xhkEeul", - "review": 0, - "should_download": 0, - "title": "Stepbro sagt, wie w\u00e4re es, wenn ich es gerade mache und dir meinen M\u00fcll zeige | xHamster", - "file_name": "Stepbro sagt, wie w\u00e4re es, wenn ich es gerade mache und dir meinen M\u00fcll zeige [xhkEeul].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/0c50a81e-4b37-436e-9c08-89ad6b42b6c1.mp4" - }, - { - "id": "0c73ce1b-a6ac-4f51-aed9-f94dcda7f763", - "created_date": "2024-08-09 19:32:55.707863", - "last_modified_date": "2024-08-16 10:27:02.744000", - "version": 1, - "url": "https://ge.xhamster.com/videos/a-sexy-strip-memory-game-turns-sexual-real-fast-with-the-lucky-losers-xhVrqcy", - "review": 0, - "should_download": 0, - "title": "Ein sexy strip-memory-spiel wird mit den gl\u00fccklichen verlierern sexuell | xHamster", - "file_name": "Ein sexy strip-memory-spiel wird mit den gl\u00fccklichen verlierern sexuell [xhVrqcy].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/0c73ce1b-a6ac-4f51-aed9-f94dcda7f763.mp4" - }, - { - "id": "0c74b46d-1692-4d36-9ded-3629899c632a", - "created_date": "2024-07-25 07:29:46.008381", - "last_modified_date": "2024-07-25 07:29:46.008381", - "version": 0, - "url": "https://ge.xhamster.com/videos/threesome-full-of-blowjobs-and-pussy-licking-3-xhznxzT", - "review": 0, - "should_download": 0, - "title": "Dreier mit Blowjobs und Muschi lecken - 3 | xHamster", - "file_name": "Dreier mit Blowjobs und Muschi lecken - 3 [xhznxzT].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0c74b46d-1692-4d36-9ded-3629899c632a.mp4" - }, - { - "id": "0c81f67a-d98f-4b12-a9c1-9a2c32a96d38", - "created_date": "2024-07-25 07:29:47.117591", - "last_modified_date": "2024-07-25 07:29:47.117591", - "version": 0, - "url": "https://ge.xhamster.com/videos/crazy-birthday-party-spiced-up-with-group-sex-games-1409430", - "review": 0, - "should_download": 0, - "title": "Verr\u00fcckte Geburtstagsfeier, aufgepeppt mit Gruppensex-Spielen | xHamster", - "file_name": "Verr\u00fcckte Geburtstagsfeier, aufgepeppt mit Gruppensex-Spielen [1409430].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0c81f67a-d98f-4b12-a9c1-9a2c32a96d38.mp4" - }, - { - "id": "0c8dda2b-7e8f-41af-850b-9fa6a22b3c2d", - "created_date": "2024-07-25 07:29:44.839063", - "last_modified_date": "2024-07-25 07:29:44.839063", - "version": 0, - "url": "https://ge.xhamster.com/videos/big-boobed-babysitter-desperate-for-cock-maxim-law-xhTLNrm", - "review": 0, - "should_download": 0, - "title": "Vollbusige Babysitterin, verzweifelt nach Schwanz - Maxime - Gesetz | xHamster", - "file_name": "Vollbusige Babysitterin, verzweifelt nach Schwanz - Maxime - Gesetz [xhTLNrm].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0c8dda2b-7e8f-41af-850b-9fa6a22b3c2d.mp4" - }, - { - "id": "0c97f71e-d550-4261-9a7c-995527f8ece3", - "created_date": "2024-12-30 18:50:10.044000", - "last_modified_date": "2025-01-03 00:24:39.724000", - "version": 1, - "url": "https://ge.xhamster.com/videos/wife-likes-to-suck-two-dicks-and-get-fucked-by-two-guys-xhH12ga", - "review": 0, - "should_download": 0, - "title": "Ehefrau mag es, zwei schw\u00e4nze zu lutschen und von zwei typen gefickt zu werden | xHamster", - "file_name": "Ehefrau mag es, zwei schw\u00e4nze zu lutschen und von zwei typen gefickt zu werden [xhH12ga].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/0c97f71e-d550-4261-9a7c-995527f8ece3.mp4" - }, - { - "id": "0cd93120-e3f9-45a3-acd9-83467501f20d", - "created_date": "2024-07-25 07:29:47.288352", - "last_modified_date": "2024-07-25 07:29:47.288352", - "version": 0, - "url": "https://ge.xhamster.com/videos/mom-caught-stepdaughter-and-teaches-her-sex-with-bbc-in-3some-xhsIj42", - "review": 0, - "should_download": 0, - "title": "Mutter erwischt Stief Tochter beim Fummeln und fickt im Dreier mit | xHamster", - "file_name": "Mutter erwischt Stief Tochter beim Fummeln und fickt im Dreier mit [xhsIj42].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0cd93120-e3f9-45a3-acd9-83467501f20d.mp4" - }, - { - "id": "0d07b1e6-b4ac-4401-8977-14aace4002ae", - "created_date": "2024-07-25 07:29:45.029452", - "last_modified_date": "2024-07-25 07:29:45.029452", - "version": 0, - "url": "https://ge.xhamster.com/videos/wild-german-babe-gets-double-penetrated-on-the-beach-xhNxJut", - "review": 0, - "should_download": 0, - "title": "Wildes deutsches Sch\u00e4tzchen wird am Strand doppelt penetriert | xHamster", - "file_name": "Wildes deutsches Sch\u00e4tzchen wird am Strand doppelt penetriert [xhNxJut].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0d07b1e6-b4ac-4401-8977-14aace4002ae.mp4" - }, - { - "id": "0d07c158-208e-404f-8856-cb5f1fe0315e", - "created_date": "2024-07-25 07:29:45.199386", - "last_modified_date": "2024-07-25 07:29:45.199386", - "version": 0, - "url": "https://ge.xhamster.com/videos/kelly-the-coed-7-xhyi1qn", - "review": 0, - "should_download": 0, - "title": "Kelly the Coed # 7 | xHamster", - "file_name": "Kelly the Coed # 7 [xhyi1qn].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/0d07c158-208e-404f-8856-cb5f1fe0315e.mp4" - }, - { - "id": "0de0991e-00dd-4b29-86ba-319ae216a52e", - "created_date": "2024-07-25 07:29:46.162594", - "last_modified_date": "2024-07-25 07:29:46.162594", - "version": 0, - "url": "https://ge.xhamster.com/videos/daddy4k-about-gentlemans-bout-xhIBo3P", - "review": 0, - "should_download": 0, - "title": "DADDY4K. \u00dcber gentleman's blowjob | xHamster", - "file_name": "DADDY4K. \u00dcber gentleman's blowjob [xhIBo3P].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0de0991e-00dd-4b29-86ba-319ae216a52e.mp4" - }, - { - "id": "0de349c2-2ad0-4906-9d00-e67621089154", - "created_date": "2024-07-25 07:29:48.085875", - "last_modified_date": "2024-07-25 07:29:48.085875", - "version": 0, - "url": "https://ge.xhamster.com/videos/young-nympho-wants-to-experiment-in-the-pool-and-fuck-wildly-xhwEMCn", - "review": 0, - "should_download": 0, - "title": "Junge Nymphomanin will im Pool experimentieren und wild ficken | xHamster", - "file_name": "Junge Nymphomanin will im Pool experimentieren und wild ficken [xhwEMCn].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0de349c2-2ad0-4906-9d00-e67621089154.mp4" - }, - { - "id": "0dfdb33b-7ad6-4f49-a58d-e3c4077c98ce", - "created_date": "2024-07-25 07:29:47.375619", - "last_modified_date": "2024-07-25 07:29:47.375619", - "version": 0, - "url": "https://ge.xhamster.com/videos/teenievision-04-teen-house-xhhSL3W", - "review": 0, - "should_download": 0, - "title": "Teenievision 04: Teenie-Haus | xHamster", - "file_name": "Teenievision 04\uff1a Teenie-Haus [xhhSL3W].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0dfdb33b-7ad6-4f49-a58d-e3c4077c98ce.mp4" - }, - { - "id": "0e16ec3d-e002-4dd5-b0bc-df58fabb8f14", - "created_date": "2024-12-29 23:53:27.911408", - "last_modified_date": "2024-12-29 23:53:27.911408", - "version": 0, - "url": "https://ge.xhamster.com/videos/au-pair-full-hd-movie-xh6jPeA", - "review": 0, - "should_download": 0, - "title": "Au-pair (Full HD-Film) | xHamster", - "file_name": "Au-pair (Full HD-Film) [xh6jPeA].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/0e16ec3d-e002-4dd5-b0bc-df58fabb8f14.mp4" - }, - { - "id": "0e4f8b79-52ca-41a7-a8d5-c7a5100f5823", - "created_date": "2024-07-25 07:29:47.179153", - "last_modified_date": "2024-07-25 07:29:47.179153", - "version": 0, - "url": "https://ge.xhamster.com/videos/fucked-on-the-boat-trip-xhP28wx", - "review": 0, - "should_download": 0, - "title": "Auf der Bootsfahrt gefickt | xHamster", - "file_name": "Auf der Bootsfahrt gefickt [xhP28wx].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0e4f8b79-52ca-41a7-a8d5-c7a5100f5823.mp4" - }, - { - "id": "0ea70223-2e5a-4e06-815c-4b9268dbd9ee", - "created_date": "2024-07-25 07:29:46.908038", - "last_modified_date": "2024-07-25 07:29:46.908038", - "version": 0, - "url": "https://ge.xhamster.com/videos/pretty-peaches-ii-1987-9301575", - "review": 0, - "should_download": 0, - "title": "H\u00fcbsche Pfirsiche ii 1987 | xHamster", - "file_name": "H\u00fcbsche Pfirsiche ii 1987 [9301575].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0ea70223-2e5a-4e06-815c-4b9268dbd9ee.mp4" - }, - { - "id": "0ebbb198-c98e-41a6-89a7-0b7234a7cbb4", - "created_date": "2024-07-25 07:29:45.297256", - "last_modified_date": "2024-07-25 07:29:45.297256", - "version": 0, - "url": "https://ge.xhamster.com/videos/18-and-confused-8-xhvgoL0", - "review": 0, - "should_download": 0, - "title": "18 und verwirrt # 8 | xHamster", - "file_name": "18 und verwirrt # 8 [xhvgoL0].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0ebbb198-c98e-41a6-89a7-0b7234a7cbb4.mp4" - }, - { - "id": "0ecb3db6-2aae-4b96-b936-d3d82bc834bb", - "created_date": "2024-07-25 07:29:46.115434", - "last_modified_date": "2024-07-25 07:29:46.115434", - "version": 0, - "url": "https://ge.xhamster.com/videos/teen-babe-gets-fucked-by-teacher-on-his-desk-xhaCa4E", - "review": 0, - "should_download": 0, - "title": "Teen-Sch\u00e4tzchen wird vom Lehrer auf seinem Schreibtisch gefickt | xHamster", - "file_name": "Teen-Sch\u00e4tzchen wird vom Lehrer auf seinem Schreibtisch gefickt [xhaCa4E].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0ecb3db6-2aae-4b96-b936-d3d82bc834bb.mp4" - }, - { - "id": "0f4a7acf-9d51-4159-a488-98f302d0f3e2", - "created_date": "2024-07-25 07:29:44.929141", - "last_modified_date": "2024-07-25 07:29:44.929141", - "version": 0, - "url": "https://ge.xhamster.com/videos/die-schone-und-das-biest-full-movie-xhNN3mC", - "review": 0, - "should_download": 0, - "title": "Die Schone Und Das Biest Full Movie, Free Porn 55 | xHamster", - "file_name": "Die Schone und das Biest (Full Movie) [xhNN3mC].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0f4a7acf-9d51-4159-a488-98f302d0f3e2.mp4" - }, - { - "id": "0f6d3ae5-9656-4087-9a41-00ddf6a35f9c", - "created_date": "2024-08-28 23:21:54.349338", - "last_modified_date": "2024-10-21 16:25:19.155000", - "version": 1, - "url": "https://ge.xhamster.com/videos/bare-family-xhh6JGc", - "review": 0, - "should_download": 0, - "title": "Nackte Familie !! | xHamster", - "file_name": "Nackte Familie !! [xhh6JGc].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/0f6d3ae5-9656-4087-9a41-00ddf6a35f9c.mp4" - }, - { - "id": "0f8967ca-ec35-45eb-8d0f-79b446def076", - "created_date": "2024-09-24 08:11:38.995251", - "last_modified_date": "2024-10-21 16:25:27.755000", - "version": 1, - "url": "https://ge.xhamster.com/videos/adult-time-stepmom-reagan-foxx-caught-her-stepson-fucking-their-milf-neighbor-and-joined-in-xh0ZgLR", - "review": 0, - "should_download": 0, - "title": "Erwachsenenzeit - Stiefmutter Reagan Foxx erwischte ihren Stiefsohn beim Ficken ihrer MILF-Nachbarin und machte mit! | xHamster", - "file_name": "Erwachsenenzeit - Stiefmutter Reagan Foxx erwischte ihren Stiefsohn beim Ficken ihrer MILF-Nachbarin und machte mit! [xh0ZgLR].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/0f8967ca-ec35-45eb-8d0f-79b446def076.mp4" - }, - { - "id": "0f8c2080-850a-44f6-bcee-a30c0454f1c8", - "created_date": "2024-07-25 07:29:44.613623", - "last_modified_date": "2024-12-30 23:04:06.578000", - "version": 1, - "url": "https://ge.xhamster.com/videos/sex-on-a-boat-10541208", - "review": 0, - "should_download": 0, - "title": "Sex auf einem Boot | xHamster", - "file_name": "Sex auf einem Boot [10541208].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/0f8c2080-850a-44f6-bcee-a30c0454f1c8.mp4" - }, - { - "id": "0fb4bd60-6fc6-48e7-a9ce-8b93345c29cc", - "created_date": "2024-07-25 07:29:44.440259", - "last_modified_date": "2024-07-25 07:29:44.440259", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-sex-xhX5gLo", - "review": 0, - "should_download": 0, - "title": "Familiensex | xHamster", - "file_name": "Familiensex [xhX5gLo].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/0fb4bd60-6fc6-48e7-a9ce-8b93345c29cc.mp4" - }, - { - "id": "10732dee-e916-4d7d-8bba-1f651da791e2", - "created_date": "2024-07-25 07:29:44.815651", - "last_modified_date": "2024-07-25 07:29:44.815651", - "version": 0, - "url": "https://ge.xhamster.com/videos/german-2-complete-film-b-r-12714004", - "review": 0, - "should_download": 0, - "title": "Deutsch # 2 - kompletter Film -b $ r | xHamster", - "file_name": "Deutsch # 2 - kompletter Film -b $ r [12714004].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/10732dee-e916-4d7d-8bba-1f651da791e2.mp4" - }, - { - "id": "108ba31b-2d1a-4f85-89c7-745f49ac1e0b", - "created_date": "2024-11-01 21:08:26.928470", - "last_modified_date": "2024-11-01 21:08:26.928470", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-redhead-nurse-loves-threesomes-and-dp-xh6PvRA", - "review": 0, - "should_download": 0, - "title": "Die rothaarige Krankenschwester liebt Dreier und Doppelpenetration | xHamster", - "file_name": "Die rothaarige Krankenschwester liebt Dreier und Doppelpenetration [xh6PvRA].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/108ba31b-2d1a-4f85-89c7-745f49ac1e0b.mp4" - }, - { - "id": "10d7db9b-92ef-48b0-8e9f-64bc3bb13bff", - "created_date": "2024-07-25 07:29:45.340635", - "last_modified_date": "2024-07-25 07:29:45.340635", - "version": 0, - "url": "https://ge.xhamster.com/videos/happy-family-350658", - "review": 0, - "should_download": 0, - "title": "Gl\u00fcckliche Familie | xHamster", - "file_name": "Gl\u00fcckliche Familie [350658].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/10d7db9b-92ef-48b0-8e9f-64bc3bb13bff.mp4" - }, - { - "id": "110694cc-38dc-46a0-8261-9f42b0aefc2f", - "created_date": "2024-07-25 07:29:46.245856", - "last_modified_date": "2024-07-25 07:29:46.245856", - "version": 0, - "url": "https://ge.xhamster.com/videos/fuck-turkey-stepbro-i-want-this-thanksgiving-dick-thinks-demi-hawks-xhDzbsd", - "review": 0, - "should_download": 0, - "title": "\"Fick Truthahn, Stiefbruder! Ich will diesen Thanksgiving-Schwanz!\" denkt, Demi-Falken | xHamster", - "file_name": "\uff02Fick Truthahn, Stiefbruder! Ich will diesen Thanksgiving-Schwanz!\uff02 denkt, Demi-Falken [xhDzbsd].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/110694cc-38dc-46a0-8261-9f42b0aefc2f.mp4" - }, - { - "id": "1147998e-a16f-4b39-b797-480f224cccf0", - "created_date": "2024-07-25 07:29:45.422771", - "last_modified_date": "2024-07-25 07:29:45.422771", - "version": 0, - "url": "https://ge.xhamster.com/videos/are-you-seriously-watching-stepsister-porn-again-mae-milano-asks-her-stepbro-s25-e8-xhD7wCB", - "review": 0, - "should_download": 0, - "title": "\"Siehst du dich ernsthaft wieder Stiefschwester-Porno an?\" Mae Milano fragt ihren Stiefbruder 25: e8 | xHamster", - "file_name": "\uff02Siehst du dich ernsthaft wieder Stiefschwester-Porno an\uff1f\uff02 Mae Milano fragt ihren Stiefbruder 25\uff1a e8 [xhD7wCB].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1147998e-a16f-4b39-b797-480f224cccf0.mp4" - }, - { - "id": "1167fed2-b9f5-42d0-9e6a-5cc11223d233", - "created_date": "2024-07-25 07:29:45.986060", - "last_modified_date": "2024-07-25 07:29:45.986060", - "version": 0, - "url": "https://ge.xhamster.com/videos/hard-threesome-and-dp-on-a-yacht-xhlshBf", - "review": 0, - "should_download": 0, - "title": "Harter Dreier und Doppelpenetration auf einer Yacht | xHamster", - "file_name": "Harter Dreier und Doppelpenetration auf einer Yacht [xhlshBf].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1167fed2-b9f5-42d0-9e6a-5cc11223d233.mp4" - }, - { - "id": "11803550-3c87-418e-8792-f9e60f86f6f1", - "created_date": "2024-07-25 07:29:45.904084", - "last_modified_date": "2024-07-25 07:29:45.904084", - "version": 0, - "url": "https://ge.xhamster.com/videos/kinky-cuisine-3226579", - "review": 0, - "should_download": 0, - "title": "Versaute K\u00fcche | xHamster", - "file_name": "Versaute K\u00fcche [3226579].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/11803550-3c87-418e-8792-f9e60f86f6f1.mp4" - }, - { - "id": "11c068aa-b62b-41b6-b788-e88a5d684447", - "created_date": "2024-09-24 08:11:39.000319", - "last_modified_date": "2024-10-21 16:25:32.881000", - "version": 1, - "url": "https://ge.xhamster.com/videos/freeuse-redhead-step-mother-will-do-anything-to-keep-her-lovely-stepsons-from-joining-the-army-xhYCE7l", - "review": 0, - "should_download": 0, - "title": "Freeuse, rothaarige Stiefmutter wird alles tun, um ihre sch\u00f6nen Stiefs\u00f6hne davon abzuhalten, zur Armee zu gehen | xHamster", - "file_name": "Freeuse, rothaarige Stiefmutter wird alles tun, um ihre sch\u00f6nen Stiefs\u00f6hne davon abzuhalten, zur Armee zu gehen [xhYCE7l].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/11c068aa-b62b-41b6-b788-e88a5d684447.mp4" - }, - { - "id": "11d28038-403f-4e10-b7ac-e6e06bd7405e", - "created_date": "2024-07-25 07:29:46.428543", - "last_modified_date": "2024-07-25 07:29:46.428543", - "version": 0, - "url": "https://ge.xhamster.com/videos/not-so-innocent-step-sister-s15-e4-xhqceki", - "review": 0, - "should_download": 0, - "title": "Nicht so unschuldige stiefschwester - s15: e4 | xHamster", - "file_name": "Nicht so unschuldige stiefschwester - s15\uff1a e4 [xhqceki].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/11d28038-403f-4e10-b7ac-e6e06bd7405e.mp4" - }, - { - "id": "1236448b-7566-4e0a-a3f5-673afa78a69d", - "created_date": "2024-07-25 07:29:46.177166", - "last_modified_date": "2024-07-25 07:29:46.177166", - "version": 0, - "url": "https://ge.xhamster.com/videos/versaute-sex-parties-2002-german-tyra-misoux-full-dvd-xh6i4d8", - "review": 0, - "should_download": 0, - "title": "Versaute Sex-Parties (2002, deutsch, Tyra Misoux, ganze DVD) | xHamster", - "file_name": "Versaute Sex-Parties (2002, deutsch, Tyra Misoux, ganze DVD) [xh6i4d8].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1236448b-7566-4e0a-a3f5-673afa78a69d.mp4" - }, - { - "id": "12dfd3e2-b308-4c66-91a7-7d29ed8a551c", - "created_date": "2024-09-24 08:11:38.998520", - "last_modified_date": "2024-10-21 16:25:38.370000", - "version": 1, - "url": "https://ge.xhamster.com/videos/when-things-go-too-far-xhA25g5", - "review": 0, - "should_download": 0, - "title": "Wenn es zu weit geht | xHamster", - "file_name": "Wenn es zu weit geht [xhA25g5].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/12dfd3e2-b308-4c66-91a7-7d29ed8a551c.mp4" - }, - { - "id": "12e016d5-5fc3-4f06-a027-68d14e338494", - "created_date": "2024-10-14 20:33:38.254151", - "last_modified_date": "2024-10-21 16:25:44.353000", - "version": 1, - "url": "https://ge.xhamster.com/videos/orgy-and-anal-sex-with-gorgeous-babes-xhHvIqI", - "review": 0, - "should_download": 0, - "title": "Orgie und Analsex mit wundersch\u00f6nen Sch\u00e4tzchen | xHamster", - "file_name": "Orgie und Analsex mit wundersch\u00f6nen Sch\u00e4tzchen [xhHvIqI].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/12e016d5-5fc3-4f06-a027-68d14e338494.mp4" - }, - { - "id": "1343795d-66eb-4df6-bf29-78291a26a541", - "created_date": "2024-07-25 07:29:44.666170", - "last_modified_date": "2024-07-25 07:29:44.666170", - "version": 0, - "url": "https://ge.xhamster.com/videos/blonde-step-sister-lena-reif-fucks-arrogant-brother-10639494", - "review": 0, - "should_download": 0, - "title": "Blonde Stiefschwester Lena Reif fickt arroganten Bruder | xHamster", - "file_name": "Blonde Stiefschwester Lena Reif fickt arroganten Bruder [10639494].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1343795d-66eb-4df6-bf29-78291a26a541.mp4" - }, - { - "id": "137eb9f8-49a1-44ac-8a79-d0def4fdae2e", - "created_date": "2024-07-25 07:29:46.332199", - "last_modified_date": "2024-07-25 07:29:46.332199", - "version": 0, - "url": "https://ge.xhamster.com/videos/nuru-massage-busty-masseuse-chanel-preston-bangs-the-stacked-shy-bunny-colby-with-her-boyfriend-xhGw2pz", - "review": 0, - "should_download": 0, - "title": "Nuru Massage - die vollbusige Masseuse Chanel Preston knallt das gestapelte sch\u00fcchterne H\u00e4schen Colby mit ihrem Freund | xHamster", - "file_name": "Nuru Massage - die vollbusige Masseuse Chanel Preston knallt das gestapelte sch\u00fcchterne H\u00e4schen Colby mit ihrem Freund [xhGw2pz].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/137eb9f8-49a1-44ac-8a79-d0def4fdae2e.mp4" - }, - { - "id": "13a42fc9-8699-4a60-8f3d-86b4fb810974", - "created_date": "2024-07-25 07:29:45.711901", - "last_modified_date": "2024-07-25 07:29:45.711901", - "version": 0, - "url": "https://ge.xhamster.com/videos/a-group-of-naked-students-are-in-the-pool-hardly-fucking-3389489", - "review": 0, - "should_download": 0, - "title": "Eine Gruppe nackter Studenten fickt im Pool kaum | xHamster", - "file_name": "Eine Gruppe nackter Studenten fickt im Pool kaum [3389489].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/13a42fc9-8699-4a60-8f3d-86b4fb810974.mp4" - }, - { - "id": "13ca65c2-5501-488f-aca6-8da54fdce0b8", - "created_date": "2024-07-25 07:29:44.269717", - "last_modified_date": "2024-07-25 07:29:44.269717", - "version": 0, - "url": "https://ge.xhamster.com/videos/step-sister-shares-brothers-bed-family-therapy-xhJoDYL", - "review": 0, - "should_download": 0, - "title": "Stiefschwester teilt das Bett des Bruders - Familientherapie | xHamster", - "file_name": "Stiefschwester teilt das Bett des Bruders - Familientherapie [xhJoDYL].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/13ca65c2-5501-488f-aca6-8da54fdce0b8.mp4" - }, - { - "id": "13d82dc3-c7ea-461a-8b19-fcb98f81ff55", - "created_date": "2024-07-25 07:29:47.198552", - "last_modified_date": "2024-07-25 07:29:47.198552", - "version": 0, - "url": "https://ge.xhamster.com/videos/two-busty-german-maids-pleasing-a-hard-pecker-with-their-tight-holes-xhB9aOa", - "review": 0, - "should_download": 0, - "title": "Zwei vollbusige deutsche Hausm\u00e4dchen befriedigen einen harten Schwanz mit ihren engen L\u00f6chern | xHamster", - "file_name": "Zwei vollbusige deutsche Hausm\u00e4dchen befriedigen einen harten Schwanz mit ihren engen L\u00f6chern [xhB9aOa].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/13d82dc3-c7ea-461a-8b19-fcb98f81ff55.mp4" - }, - { - "id": "1408c1b9-7f67-45dd-812c-b4b4d3216f38", - "created_date": "2024-07-25 07:29:47.210281", - "last_modified_date": "2024-07-25 07:29:47.210281", - "version": 0, - "url": "https://ge.xhamster.com/videos/schoolgirl-eduvation-ccc-german-dub-12366609", - "review": 0, - "should_download": 0, - "title": "Schulm\u00e4dchen Eduvation - ccc german dub | xHamster", - "file_name": "Schulm\u00e4dchen Eduvation - ccc german dub [12366609].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1408c1b9-7f67-45dd-812c-b4b4d3216f38.mp4" - }, - { - "id": "147fcbe6-a484-4782-9641-10ba0d77a437", - "created_date": "2024-07-25 07:29:48.041213", - "last_modified_date": "2024-07-25 07:29:48.041213", - "version": 0, - "url": "https://ge.xhamster.com/videos/free-and-perverse-14904792", - "review": 0, - "should_download": 0, - "title": "Frei und pervers | xHamster", - "file_name": "Frei und pervers [14904792].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/147fcbe6-a484-4782-9641-10ba0d77a437.mp4" - }, - { - "id": "14c86b57-e955-4b35-b251-0f2a61962f2a", - "created_date": "2024-07-25 07:29:46.077512", - "last_modified_date": "2024-07-25 07:29:46.077512", - "version": 0, - "url": "https://ge.xhamster.com/videos/students-party-one-girl-3-guys-2206707", - "review": 0, - "should_download": 0, - "title": "Studenten feiern ein M\u00e4dchen 3 Typen | xHamster", - "file_name": "Studenten feiern ein M\u00e4dchen 3 Typen [2206707].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/14c86b57-e955-4b35-b251-0f2a61962f2a.mp4" - }, - { - "id": "14d871a2-bd92-4e0d-91af-e6150e3e2156", - "created_date": "2024-07-25 07:29:45.722334", - "last_modified_date": "2024-07-25 07:29:45.722334", - "version": 0, - "url": "https://ge.xhamster.com/videos/nasse-nymphen-2331575", - "review": 0, - "should_download": 0, - "title": "Nasse Nymphen: Free Anal Porn Video 5c | xHamster", - "file_name": "Nasse Nymphen [2331575].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/14d871a2-bd92-4e0d-91af-e6150e3e2156.mp4" - }, - { - "id": "151dfaa8-d1f5-4fac-9864-0b007ecc6059", - "created_date": "2024-07-25 07:29:47.059735", - "last_modified_date": "2024-07-25 07:29:47.059735", - "version": 0, - "url": "https://ge.xhamster.com/videos/teen-students-dickriding-after-oral-pleasing-12427252", - "review": 0, - "should_download": 0, - "title": "Teen Teen Studenten Schwanz reiten nach oraler Befriedigung | xHamster", - "file_name": "Teen Teen Studenten Schwanz reiten nach oraler Befriedigung [12427252].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/151dfaa8-d1f5-4fac-9864-0b007ecc6059.mp4" - }, - { - "id": "15439c23-ac16-4739-914b-8fcf5d309762", - "created_date": "2024-07-25 07:29:44.539947", - "last_modified_date": "2024-07-25 07:29:44.539947", - "version": 0, - "url": "https://ge.xhamster.com/videos/cum-in-my-mouth-13446477", - "review": 0, - "should_download": 0, - "title": "Sperma auf der Zunge | xHamster", - "file_name": "Sperma auf der Zunge [13446477].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/15439c23-ac16-4739-914b-8fcf5d309762.mp4" - }, - { - "id": "1594945a-3019-42a2-9093-1368e16693cf", - "created_date": "2024-07-25 07:29:46.374380", - "last_modified_date": "2024-07-25 07:29:46.374380", - "version": 0, - "url": "https://ge.xhamster.com/videos/die-neue-assistentin-auf-dem-billardtisch-eingearbeitet-xhRN4kT", - "review": 0, - "should_download": 0, - "title": "Die Neue Assistentin Auf Dem Billardtisch Eingearbeitet | xHamster", - "file_name": "Die neue Assistentin auf dem Billardtisch eingearbeitet [xhRN4kT].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1594945a-3019-42a2-9093-1368e16693cf.mp4" - }, - { - "id": "15efb478-81d6-4028-80de-2959c44d48b0", - "created_date": "2024-07-25 07:29:44.599317", - "last_modified_date": "2024-07-25 07:29:44.599317", - "version": 0, - "url": "https://ge.xhamster.com/videos/juicy-white-dick-plows-young-cunt-12812057", - "review": 0, - "should_download": 0, - "title": "Saftiger wei\u00dfer Schwanz pfl\u00fcgt junge Fotze | xHamster", - "file_name": "Saftiger wei\u00dfer Schwanz pfl\u00fcgt junge Fotze [12812057].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/15efb478-81d6-4028-80de-2959c44d48b0.mp4" - }, - { - "id": "16317d33-8302-4ac0-84db-09a742466149", - "created_date": "2024-08-16 11:21:33.106000", - "last_modified_date": "2024-10-21 16:25:50.795000", - "version": 1, - "url": "https://ge.xhamster.com/videos/how-to-ace-the-job-interview-at-the-free-use-office-feat-vanna-bardot-millie-morgan-freeuse-milf-xh0EtlP", - "review": 0, - "should_download": 0, - "title": "Wie man das Vorstellungsgespr\u00e4ch bei der kostenlosen B\u00fcronutzung acesiert. Vanna bardot & millie morgan - freie milf | xHamster", - "file_name": "Wie man das Vorstellungsgespr\u00e4ch bei der kostenlosen B\u00fcronutzung acesiert. Vanna bardot & millie morgan - freie milf [xh0EtlP].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/16317d33-8302-4ac0-84db-09a742466149.mp4" - }, - { - "id": "16b85580-9de3-4756-b3cd-ed4976d1987f", - "created_date": "2024-07-25 07:29:48.078642", - "last_modified_date": "2024-07-25 07:29:48.078642", - "version": 0, - "url": "https://ge.xhamster.com/videos/brother-fucked-stepsisters-on-family-vacation-xhrUAe6", - "review": 0, - "should_download": 0, - "title": "Bruder fickte Stiefschwestern im Familienurlaub | xHamster", - "file_name": "Bruder fickte Stiefschwestern im Familienurlaub [xhrUAe6].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/16b85580-9de3-4756-b3cd-ed4976d1987f.mp4" - }, - { - "id": "16e42995-8665-4fbf-8726-4d6f9484bb25", - "created_date": "2024-07-25 07:29:47.704233", - "last_modified_date": "2024-07-25 07:29:47.704233", - "version": 0, - "url": "https://ge.xhamster.com/videos/swap-bro-says-i-cant-stop-thinking-about-fucking-you-both-xhLFBE5", - "review": 0, - "should_download": 0, - "title": "Swap-Bro, sagt, ich kann nicht aufh\u00f6ren, dar\u00fcber nachzudenken, dich beide zu ficken | xHamster", - "file_name": "Swap-Bro, sagt, ich kann nicht aufh\u00f6ren, dar\u00fcber nachzudenken, dich beide zu ficken [xhLFBE5].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/16e42995-8665-4fbf-8726-4d6f9484bb25.mp4" - }, - { - "id": "17043616-6ae0-41d7-a240-03f85bbb8581", - "created_date": "2024-07-25 07:29:44.828601", - "last_modified_date": "2024-07-25 07:29:44.828601", - "version": 0, - "url": "https://ge.xhamster.com/videos/hes-really-hot-i-bet-he-has-a-big-dick-xhGfU90", - "review": 0, - "should_download": 0, - "title": "Er ist wirklich hei\u00df, ich wette, er hat einen gro\u00dfen Schwanz! | xHamster", - "file_name": "Er ist wirklich hei\u00df, ich wette, er hat einen gro\u00dfen Schwanz! [xhGfU90].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/17043616-6ae0-41d7-a240-03f85bbb8581.mp4" - }, - { - "id": "173f7ca2-11df-402b-a6be-185c20c7503e", - "created_date": "2024-07-25 07:29:46.122714", - "last_modified_date": "2024-07-25 07:29:46.122714", - "version": 0, - "url": "https://ge.xhamster.com/videos/nude-beach-orgy-5234700", - "review": 0, - "should_download": 0, - "title": "FKK-Strand-Orgie | xHamster", - "file_name": "FKK-Strand-Orgie [5234700].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/173f7ca2-11df-402b-a6be-185c20c7503e.mp4" - }, - { - "id": "176c333c-a9c3-48a9-b7b4-83cc2a3b5fe5", - "created_date": "2024-07-25 07:29:47.386625", - "last_modified_date": "2024-07-25 07:29:47.386625", - "version": 0, - "url": "https://ge.xhamster.com/videos/surprise-gangbang-meridian-14004716", - "review": 0, - "should_download": 0, - "title": "\u00dcberraschungs-Gangbang Meridian | xHamster", - "file_name": "\u00dcberraschungs-Gangbang Meridian [14004716].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/176c333c-a9c3-48a9-b7b4-83cc2a3b5fe5.mp4" - }, - { - "id": "17c37cf5-6d6d-41a6-9d53-1d1b0f460d74", - "created_date": "2024-07-25 07:29:46.780061", - "last_modified_date": "2024-07-25 07:29:46.780061", - "version": 0, - "url": "https://ge.xhamster.com/videos/gang-bang-my-wife-scene-12-xhGnY6Q", - "review": 0, - "should_download": 0, - "title": "Gangbang mit meiner Ehefrau - Szene # 12 | xHamster", - "file_name": "Gangbang mit meiner Ehefrau - Szene # 12 [xhGnY6Q].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/17c37cf5-6d6d-41a6-9d53-1d1b0f460d74.mp4" - }, - { - "id": "17d51cb3-dbce-4879-ae7a-4276ec1cadc3", - "created_date": "2024-07-25 07:29:47.510728", - "last_modified_date": "2024-07-25 07:29:47.510728", - "version": 0, - "url": "https://ge.xhamster.com/videos/tushy-jessa-rhodes-craves-two-cocks-in-amazing-dp-sex-11034717", - "review": 0, - "should_download": 0, - "title": "Tushy Jessa Rhodes sehnt sich nach zwei Schw\u00e4nzen in erstaunlichem Doppelpenetrations-Sex | xHamster", - "file_name": "Tushy Jessa Rhodes sehnt sich nach zwei Schw\u00e4nzen in erstaunlichem Doppelpenetrations-Sex [11034717].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/17d51cb3-dbce-4879-ae7a-4276ec1cadc3.mp4" - }, - { - "id": "184b0462-705b-497d-8f32-2936492f2260", - "created_date": "2024-07-25 07:29:47.755820", - "last_modified_date": "2024-07-25 07:29:47.755820", - "version": 0, - "url": "https://ge.xhamster.com/videos/couple-gets-horny-female-massage-therapist-xh6xaSe", - "review": 0, - "should_download": 0, - "title": "Paar bekommt geile weibliche Masseurin | xHamster", - "file_name": "Paar bekommt geile weibliche Masseurin [xh6xaSe].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/184b0462-705b-497d-8f32-2936492f2260.mp4" - }, - { - "id": "185b5318-238b-426b-b4f4-f617a9763eb2", - "created_date": "2024-07-25 07:29:47.464957", - "last_modified_date": "2024-07-25 07:29:47.464957", - "version": 0, - "url": "https://ge.xhamster.com/videos/a-group-of-college-teenagers-playing-funny-sex-games-1409432", - "review": 0, - "should_download": 0, - "title": "Eine Gruppe von College-Teenagern, die lustige Sexspiele spielen | xHamster", - "file_name": "Eine Gruppe von College-Teenagern, die lustige Sexspiele spielen [1409432].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/185b5318-238b-426b-b4f4-f617a9763eb2.mp4" - }, - { - "id": "18677cf7-29cf-4dc5-a7e3-f6d6174670d0", - "created_date": "2024-07-25 07:29:46.760907", - "last_modified_date": "2024-07-25 07:29:46.760907", - "version": 0, - "url": "https://ge.xhamster.com/videos/darling-assfucked-in-the-vineyard-13623559", - "review": 0, - "should_download": 0, - "title": "Jane Darling wird im Weinberg arschgefickt | xHamster", - "file_name": "Jane Darling wird im Weinberg arschgefickt [13623559].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/18677cf7-29cf-4dc5-a7e3-f6d6174670d0.mp4" - }, - { - "id": "1886466c-7006-4482-a62c-75ff0bffea62", - "created_date": "2024-07-25 07:29:45.225976", - "last_modified_date": "2024-07-25 07:29:45.225976", - "version": 0, - "url": "https://ge.xhamster.com/videos/a-family-evening-xhryxr8", - "review": 0, - "should_download": 0, - "title": "Ein Familienabend | xHamster", - "file_name": "Ein Familienabend [xhryxr8].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1886466c-7006-4482-a62c-75ff0bffea62.mp4" - }, - { - "id": "1889d936-679d-4a06-89a2-40a4e24325a3", - "created_date": "2024-07-25 07:29:44.338453", - "last_modified_date": "2024-07-25 07:29:44.338453", - "version": 0, - "url": "https://ge.xhamster.com/videos/decided-to-walk-naked-into-his-stepsisters-room-xhOCdLI", - "review": 0, - "should_download": 0, - "title": "Beschloss, nackt in das zimmer seiner stiefschwester zu gehen | xHamster", - "file_name": "Beschloss, nackt in das zimmer seiner stiefschwester zu gehen [xhOCdLI].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1889d936-679d-4a06-89a2-40a4e24325a3.mp4" - }, - { - "id": "18a2fb00-14fc-4667-b777-cdd4af361e2c", - "created_date": "2024-08-28 23:21:54.364504", - "last_modified_date": "2024-08-28 23:21:54.364504", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-hubby-fucks-your-wife-in-front-of-us-xh8sNxF", - "review": 0, - "should_download": 0, - "title": "Mein ehemann fickt deine ehefrau vor uns | xHamster", - "file_name": "Mein ehemann fickt deine ehefrau vor uns [xh8sNxF].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/18a2fb00-14fc-4667-b777-cdd4af361e2c.mp4" - }, - { - "id": "18d70c6f-89e5-413f-b300-f04aa8737dc0", - "created_date": "2024-07-25 07:29:47.801289", - "last_modified_date": "2024-07-25 07:29:47.801289", - "version": 0, - "url": "https://ge.xhamster.com/videos/wowgirls-michelle-red-fox-and-the-lucky-guy-in-a-hot-fuck-scene-xh3gx6r", - "review": 0, - "should_download": 0, - "title": "Wowgirls - Michelle Red Fox und der Gl\u00fcckspilz in einer hei\u00dfen Fickszene | xHamster", - "file_name": "Wowgirls - Michelle Red Fox und der Gl\u00fcckspilz in einer hei\u00dfen Fickszene [xh3gx6r].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/18d70c6f-89e5-413f-b300-f04aa8737dc0.mp4" - }, - { - "id": "18ecad86-c84f-4315-90c4-da14613a35ac", - "created_date": "2024-07-25 07:29:46.420575", - "last_modified_date": "2024-07-25 07:29:46.420575", - "version": 0, - "url": "https://ge.xhamster.com/videos/wild-and-crazy-girlfriend-vacation-14527087", - "review": 0, - "should_download": 0, - "title": "Wilder und verr\u00fcckter Urlaub mit Freundin | xHamster", - "file_name": "Wilder und verr\u00fcckter Urlaub mit Freundin [14527087].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/18ecad86-c84f-4315-90c4-da14613a35ac.mp4" - }, - { - "id": "18faa3f3-b87b-4400-bd2b-5b24ad1ba386", - "created_date": "2024-07-25 07:29:45.355684", - "last_modified_date": "2024-07-25 07:29:45.355684", - "version": 0, - "url": "https://ge.xhamster.com/videos/madchen-internat-2-xhasv9y", - "review": 0, - "should_download": 0, - "title": "Madchen Internat 2: Free Porn Video 93 | xHamster", - "file_name": "Madchen internat 2 [xhasv9y].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/18faa3f3-b87b-4400-bd2b-5b24ad1ba386.mp4" - }, - { - "id": "1902ad09-cf29-489e-963c-821a09d8cb68", - "created_date": "2024-07-25 07:29:44.286339", - "last_modified_date": "2024-07-25 07:29:44.286339", - "version": 0, - "url": "https://ge.xhamster.com/videos/step-daughter-catches-scandalous-step-mom-seducing-her-weak-boyfriend-xh2I9PB", - "review": 0, - "should_download": 0, - "title": "Stieftochter erwischt skandal\u00f6se Stiefmutter, die ihren schwachen Freund verf\u00fchrt | xHamster", - "file_name": "Stieftochter erwischt skandal\u00f6se Stiefmutter, die ihren schwachen Freund verf\u00fchrt [xh2I9PB].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1902ad09-cf29-489e-963c-821a09d8cb68.mp4" - }, - { - "id": "195e7c52-43b2-42ed-9475-a144f7e8d072", - "created_date": "2024-11-10 16:53:33.465101", - "last_modified_date": "2024-11-10 16:53:33.465101", - "version": 0, - "url": "https://ge.xhamster.com/videos/a-hot-unfaithful-wife-full-movie-xhcJZSS", - "review": 0, - "should_download": 0, - "title": "Eine hei\u00dfe untreue Ehefrau! (kompletter Film) | xHamster", - "file_name": "Eine hei\u00dfe untreue Ehefrau! (kompletter Film) [xhcJZSS].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/195e7c52-43b2-42ed-9475-a144f7e8d072.mp4" - }, - { - "id": "19610b21-0900-4abc-8b67-54ca043a55e7", - "created_date": "2024-07-25 07:29:44.432278", - "last_modified_date": "2024-07-25 07:29:44.432278", - "version": 0, - "url": "https://ge.xhamster.com/videos/blondine-zum-fremdfick-ueberredet-xh4ektS", - "review": 0, - "should_download": 0, - "title": "Blondine Zum Fremdfick Ueberredet, Free Porn 78 | xHamster", - "file_name": "Blondine zum fremdfick ueberredet [xh4ektS].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/19610b21-0900-4abc-8b67-54ca043a55e7.mp4" - }, - { - "id": "196e5626-0dea-431e-b628-a4efdc7cbf02", - "created_date": "2024-08-16 11:11:33.384000", - "last_modified_date": "2024-10-21 16:25:59.964000", - "version": 1, - "url": "https://ge.xhamster.com/videos/sturmfreie-bude-full-movie-xhQA3jC", - "review": 0, - "should_download": 0, - "title": "Sturmfreie Bude Full Movie, Free German Porn 49 | xHamster", - "file_name": "Sturmfreie Bude (Full Movie) [xhQA3jC].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/196e5626-0dea-431e-b628-a4efdc7cbf02.mp4" - }, - { - "id": "1a45e084-c4af-4c9f-877c-e4cddd87e4a3", - "created_date": "2024-07-25 07:29:45.643475", - "last_modified_date": "2024-07-25 07:29:45.643475", - "version": 0, - "url": "https://ge.xhamster.com/videos/fucking-stepsister-and-her-friend-xhfoMPh", - "review": 0, - "should_download": 0, - "title": "Stiefschwester und ihre Freundin gefickt | xHamster", - "file_name": "Stiefschwester und ihre Freundin gefickt [xhfoMPh].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1a45e084-c4af-4c9f-877c-e4cddd87e4a3.mp4" - }, - { - "id": "1a4efc43-b6d8-49ef-818c-2eb0926fa97a", - "created_date": "2024-10-21 15:08:43.554935", - "last_modified_date": "2024-10-21 16:26:05.537000", - "version": 1, - "url": "https://ge.xhamster.com/videos/beautiful-wife-shared-with-a-perfect-stranger-she-just-met-xhSPAr0", - "review": 0, - "should_download": 0, - "title": "Sch\u00f6ne Ehefrau mit einem perfekten Fremden geteilt, den sie gerade getroffen hat | xHamster", - "file_name": "Sch\u00f6ne Ehefrau mit einem perfekten Fremden geteilt, den sie gerade getroffen hat [xhSPAr0].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/1a4efc43-b6d8-49ef-818c-2eb0926fa97a.mp4" - }, - { - "id": "1a6b8e5e-2750-4bfb-9796-5a087a760171", - "created_date": "2025-01-16 19:59:59.779771", - "last_modified_date": "2025-01-16 19:59:59.779777", - "version": 0, - "url": "https://ge.xhamster.com/videos/horny-stepdaughter-myra-moans-tries-new-clothes-and-rough-foursome-with-hot-stepmom-jessica-ryan-xhWJ33m", - "review": 0, - "should_download": 0, - "title": "Die geile stieftochter Myra Moans probiert neue kleider und groben vierer mit der hei\u00dfen stiefmutter Jessica Ryan | xHamster", - "file_name": "Die geile stieftochter Myra Moans probiert neue kleider und groben vierer mit der hei\u00dfen stiefmutter Jessica Ryan [xhWJ33m].mp4", - "path": null, - "cloud_link": "/data/media/1a6b8e5e-2750-4bfb-9796-5a087a760171.mp4" - }, - { - "id": "1a9db7f8-b0cb-4aca-9bcb-13948359415f", - "created_date": "2025-01-03 00:23:56.611000", - "last_modified_date": "2025-01-03 01:48:13.298000", - "version": 3, - "url": "https://ge.xhamster.com/videos/sechs-schwedinnen-von-der-tankstelle-xhOSyVS", - "review": 0, - "should_download": 0, - "title": "Sechs Schwedinnen Von Der Tankstelle, HD Porn c4 | xHamster", - "file_name": "Sechs Schwedinnen Von Der Tankstelle [xhOSyVS].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/1a9db7f8-b0cb-4aca-9bcb-13948359415f.mp4" - }, - { - "id": "1ab8adcd-7a1b-47cd-91d4-66b48dbc03d9", - "created_date": "2024-07-25 07:29:45.650368", - "last_modified_date": "2024-07-25 07:29:45.650368", - "version": 0, - "url": "https://ge.xhamster.com/videos/trio-anal-10472743", - "review": 0, - "should_download": 0, - "title": "Trio anal | xHamster", - "file_name": "Trio anal [10472743].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1ab8adcd-7a1b-47cd-91d4-66b48dbc03d9.mp4" - }, - { - "id": "1ae765f7-bdf0-48c0-9697-f28194efdc34", - "created_date": "2024-07-25 07:29:46.757363", - "last_modified_date": "2024-07-25 07:29:46.757363", - "version": 0, - "url": "https://ge.xhamster.com/videos/pool-cleaner-fucks-the-daughter-by-the-owner-12803404", - "review": 0, - "should_download": 0, - "title": "Pool Reiniger fickt die Tochter vom Eigent\u00fcmer | xHamster", - "file_name": "Pool Reiniger fickt die Tochter vom Eigent\u00fcmer [12803404].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1ae765f7-bdf0-48c0-9697-f28194efdc34.mp4" - }, - { - "id": "1afef720-efef-4f72-a196-c9b364321ba2", - "created_date": "2024-07-25 07:29:44.872265", - "last_modified_date": "2024-07-25 07:29:44.872265", - "version": 0, - "url": "https://ge.xhamster.com/videos/silly-games-get-my-step-sister-to-fuck-and-swallow-s6-e5-9033570", - "review": 0, - "should_download": 0, - "title": "Silly Games bringen meine Stiefschwester dazu, s6: e5 zu ficken und zu schlucken | xHamster", - "file_name": "Silly Games bringen meine Stiefschwester dazu, s6\uff1a e5 zu ficken und zu schlucken [9033570].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1afef720-efef-4f72-a196-c9b364321ba2.mp4" - }, - { - "id": "1b04b3a5-200b-45f4-bb40-54d9c850fb8a", - "created_date": "2024-07-25 07:29:45.741757", - "last_modified_date": "2024-07-25 07:29:45.741757", - "version": 0, - "url": "https://ge.xhamster.com/videos/meine-geile-nachbarin-12-9178748", - "review": 0, - "should_download": 0, - "title": "Meine Geile Nachbarin 12, Free German Porn 24 | xHamster", - "file_name": "Meine Geile Nachbarin 12 [9178748].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1b04b3a5-200b-45f4-bb40-54d9c850fb8a.mp4" - }, - { - "id": "1bab7852-6f48-4326-8170-b8aeeedb9c1e", - "created_date": "2024-07-25 07:29:46.736640", - "last_modified_date": "2024-07-25 07:29:46.736640", - "version": 0, - "url": "https://ge.xhamster.com/videos/schulmadchen-sommer-lust-und-bauernlummel-1065773", - "review": 0, - "should_download": 0, - "title": "Schulmadchen - Sommer Lust Und Bauernlummel: Free Porn 10 | xHamster", - "file_name": "Schulmadchen - Sommer, Lust Und Bauernlummel [1065773].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1bab7852-6f48-4326-8170-b8aeeedb9c1e.mp4" - }, - { - "id": "1baf7b64-87a2-4365-8b50-73c63763ec3e", - "created_date": "2024-07-25 07:29:45.308351", - "last_modified_date": "2024-07-25 07:29:45.308351", - "version": 0, - "url": "https://ge.xhamster.com/videos/sommer-des-sex-full-movie-xhbVEU7", - "review": 0, - "should_download": 0, - "title": "Sommer des sex (kompletter Film) | xHamster", - "file_name": "Sommer des sex (kompletter Film) [xhbVEU7].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1baf7b64-87a2-4365-8b50-73c63763ec3e.mp4" - }, - { - "id": "1c35d1db-1ec5-4b49-ab21-2beafa8c3a10", - "created_date": "2024-07-25 07:29:46.858233", - "last_modified_date": "2024-07-25 07:29:46.858233", - "version": 0, - "url": "https://ge.xhamster.com/videos/familienschweinchen-xh1vbrg", - "review": 0, - "should_download": 0, - "title": "Familienschweinchen: Free Porn Video 63 | xHamster", - "file_name": "Familienschweinchen [xh1vbrg].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1c35d1db-1ec5-4b49-ab21-2beafa8c3a10.mp4" - }, - { - "id": "1c434846-349b-47f7-a3e8-884d524c5e4b", - "created_date": "2024-07-25 07:29:47.593156", - "last_modified_date": "2024-07-25 07:29:47.593156", - "version": 0, - "url": "https://ge.xhamster.com/videos/every-boss-needs-an-employee-like-chanel-preston-2189041", - "review": 0, - "should_download": 0, - "title": "Jeder Chef braucht einen Angestellten wie Chanel Preston | xHamster", - "file_name": "Jeder Chef braucht einen Angestellten wie Chanel Preston [2189041].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1c434846-349b-47f7-a3e8-884d524c5e4b.mp4" - }, - { - "id": "1c5944f7-54c3-4384-a0ef-5b248e6f1f93", - "created_date": "2024-07-25 07:29:47.326537", - "last_modified_date": "2024-07-25 07:29:47.326537", - "version": 0, - "url": "https://ge.xhamster.com/videos/party-hardcore-gone-crazy-35-amateur-edit-xhlb8Jp", - "review": 0, - "should_download": 0, - "title": "Party-Hardcore verr\u00fcckt 35 - Amateur-Edit | xHamster", - "file_name": "Party-Hardcore verr\u00fcckt 35 - Amateur-Edit [xhlb8Jp].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1c5944f7-54c3-4384-a0ef-5b248e6f1f93.mp4" - }, - { - "id": "1c634605-d6b7-4c5c-8bd5-59391305ad32", - "created_date": "2024-07-25 07:29:47.268555", - "last_modified_date": "2024-07-25 07:29:47.268555", - "version": 0, - "url": "https://ge.xhamster.com/videos/vixen-sex-with-my-boss-7269259", - "review": 0, - "should_download": 0, - "title": "Vixen - Sex mit meinem Chef | xHamster", - "file_name": "Vixen - Sex mit meinem Chef [7269259].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/1c634605-d6b7-4c5c-8bd5-59391305ad32.mp4" - }, - { - "id": "1c83e2b6-9ee1-499a-b5c9-31da57018f1a", - "created_date": "2024-07-25 07:29:47.589661", - "last_modified_date": "2024-07-25 07:29:47.589661", - "version": 0, - "url": "https://ge.xhamster.com/videos/bockingen-xh9Qxth", - "review": 0, - "should_download": 0, - "title": "Bockingen | xHamster", - "file_name": "Bockingen [xh9Qxth].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1c83e2b6-9ee1-499a-b5c9-31da57018f1a.mp4" - }, - { - "id": "1ca21517-0879-41f4-a40e-8b31fc31444e", - "created_date": "2024-07-25 07:29:44.352116", - "last_modified_date": "2024-07-25 07:29:44.352116", - "version": 0, - "url": "https://ge.xhamster.com/videos/truth-or-dare-1-6186830", - "review": 0, - "should_download": 0, - "title": "Truth or Dare 1: Free Threesome HD Porn Video e7 | xHamster", - "file_name": "Truth or Dare 1 [6186830].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1ca21517-0879-41f4-a40e-8b31fc31444e.mp4" - }, - { - "id": "1ccef46d-730c-44c1-8ac3-360ab8fe9015", - "created_date": "2024-07-25 07:29:47.839401", - "last_modified_date": "2024-07-25 07:29:47.839401", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsis-says-take-off-your-clothes-so-i-can-see-you-naked-xhzOa0C", - "review": 0, - "should_download": 0, - "title": "Stiefschwester sagt, zieh deine Kleidung aus, damit ich dich nackt sehen kann! | xHamster", - "file_name": "Stiefschwester sagt, zieh deine Kleidung aus, damit ich dich nackt sehen kann! [xhzOa0C].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1ccef46d-730c-44c1-8ac3-360ab8fe9015.mp4" - }, - { - "id": "1cf691e5-3fb3-4c83-8aa2-bdfafc62ebb4", - "created_date": "2024-07-25 07:29:47.491314", - "last_modified_date": "2024-07-25 07:29:47.491314", - "version": 0, - "url": "https://ge.xhamster.com/videos/familienskandal-7259344", - "review": 0, - "should_download": 0, - "title": "Familienskandal: Free Anal Porn Video 0c | xHamster", - "file_name": "Familienskandal [7259344].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1cf691e5-3fb3-4c83-8aa2-bdfafc62ebb4.mp4" - }, - { - "id": "1d466252-dae4-461e-b680-dc213e45891a", - "created_date": "2024-07-25 07:29:45.304556", - "last_modified_date": "2024-07-25 07:29:45.304556", - "version": 0, - "url": "https://ge.xhamster.com/videos/you-should-come-lick-it-off-madison-summers-tells-stepbro-s21-e6-xhfCo0X", - "review": 0, - "should_download": 0, - "title": "\"Du solltest kommen und es ablecken\", sagt Madison Summers zum Stiefbruder -s21: e6 | xHamster", - "file_name": "\uff02Du solltest kommen und es ablecken\uff02, sagt Madison Summers zum Stiefbruder -s21\uff1a e6 [xhfCo0X].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1d466252-dae4-461e-b680-dc213e45891a.mp4" - }, - { - "id": "1d62abf0-adc1-4281-a4e5-efc65bd5da10", - "created_date": "2024-08-09 21:17:53.932680", - "last_modified_date": "2024-08-16 10:27:23.346000", - "version": 1, - "url": "https://ge.xhamster.com/videos/wij-12195485", - "review": 0, - "should_download": 0, - "title": "Wij: HD Porn Video 6a | xHamster", - "file_name": "Wij [12195485].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/1d62abf0-adc1-4281-a4e5-efc65bd5da10.mp4" - }, - { - "id": "1d652293-0045-4c03-93f7-a38b38d52890", - "created_date": "2024-07-25 07:29:44.480812", - "last_modified_date": "2024-07-25 07:29:44.480812", - "version": 0, - "url": "https://ge.xhamster.com/videos/secretary-14389288", - "review": 0, - "should_download": 0, - "title": "Sekret\u00e4rin | xHamster", - "file_name": "Sekret\u00e4rin [14389288].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1d652293-0045-4c03-93f7-a38b38d52890.mp4" - }, - { - "id": "1d6c6148-3174-4964-a6b2-5ad929f4fccd", - "created_date": "2024-11-01 21:08:26.935627", - "last_modified_date": "2024-11-01 21:08:26.935627", - "version": 0, - "url": "https://ge.xhamster.com/videos/vintage-70s-die-liebes-insel-02-xhWACK9", - "review": 0, - "should_download": 0, - "title": "Vintage 70s - Die Liebes-insel - 02, Free Porn 34 | xHamster", - "file_name": "vintage 70s - Die Liebes-Insel - 02 [xhWACK9].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/1d6c6148-3174-4964-a6b2-5ad929f4fccd.mp4" - }, - { - "id": "1da53abe-8fa9-4bfd-9942-4a2f7a6b2f8a", - "created_date": "2024-07-25 07:29:45.673619", - "last_modified_date": "2024-07-25 07:29:45.673619", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-strokes-stepsiblings-get-sexual-on-family-vacation-xhBnMSE", - "review": 0, - "should_download": 0, - "title": "In Family Strokes werden Stiefgeschwister im Familienurlaub sexuell | xHamster", - "file_name": "In Family Strokes werden Stiefgeschwister im Familienurlaub sexuell [xhBnMSE].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/1da53abe-8fa9-4bfd-9942-4a2f7a6b2f8a.mp4" - }, - { - "id": "1decc124-b356-47ca-898c-0317bb53f367", - "created_date": "2024-07-25 07:29:46.449719", - "last_modified_date": "2024-07-25 07:29:46.449719", - "version": 0, - "url": "https://ge.xhamster.com/videos/what-would-i-do-to-my-stepbrother-s30-e2-xhqQlGG", - "review": 0, - "should_download": 0, - "title": "Was w\u00fcrde ich mit meinem stiefbruer machen - s30: e2 | xHamster", - "file_name": "Was w\u00fcrde ich mit meinem stiefbruer machen - s30\uff1a e2 [xhqQlGG].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1decc124-b356-47ca-898c-0317bb53f367.mp4" - }, - { - "id": "1e1169b4-1fb5-4993-9edc-cd467bb628f9", - "created_date": "2024-07-25 07:29:45.708198", - "last_modified_date": "2024-07-25 07:29:45.708198", - "version": 0, - "url": "https://ge.xhamster.com/videos/hot-babysitter-dolly-leigh-gets-stuck-and-fucked-xhtx57x", - "review": 0, - "should_download": 0, - "title": "Die hei\u00dfe Babysitterin Dolly Leigh bleibt stecken und wird gefickt | xHamster", - "file_name": "Die hei\u00dfe Babysitterin Dolly Leigh bleibt stecken und wird gefickt [xhtx57x].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/1e1169b4-1fb5-4993-9edc-cd467bb628f9.mp4" - }, - { - "id": "1eb6b9bb-2484-499a-a973-8a01b4bc6c29", - "created_date": "2024-07-25 07:29:48.070939", - "last_modified_date": "2024-07-25 07:29:48.070939", - "version": 0, - "url": "https://ge.xhamster.com/videos/you-should-spend-more-time-outdoors-11-6809804", - "review": 0, - "should_download": 0, - "title": "Sie sollten mehr Zeit im Freien verbringen # 11 | xHamster", - "file_name": "Sie sollten mehr Zeit im Freien verbringen # 11 [6809804].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1eb6b9bb-2484-499a-a973-8a01b4bc6c29.mp4" - }, - { - "id": "1ec67004-532d-4fd0-bdeb-fc7526e9d3f0", - "created_date": "2024-07-25 07:29:44.454997", - "last_modified_date": "2024-07-25 07:29:44.454997", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsis-says-do-you-have-a-hard-on-xhdeAeu", - "review": 0, - "should_download": 0, - "title": "Stiefschwester sagt, hast du es hart? | xHamster", - "file_name": "Stiefschwester sagt, hast du es hart\uff1f [xhdeAeu].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1ec67004-532d-4fd0-bdeb-fc7526e9d3f0.mp4" - }, - { - "id": "1eed7a37-b462-403c-adf6-6120361b0c1b", - "created_date": "2024-09-24 08:11:39.000648", - "last_modified_date": "2024-10-21 16:26:12.693000", - "version": 1, - "url": "https://ge.xhamster.com/videos/4er-sex-loyalty-test-confused-stepbrother-and-husband-of-my-girlfriend-xh3o44U", - "review": 0, - "should_download": 0, - "title": "4er Sex?! TREUETEST!!! Stiefbruder und Ehemann meiner Freundin verwechselt ! | xHamster", - "file_name": "4er Sex\uff1f! TREUETEST!!! Stiefbruder und Ehemann meiner Freundin verwechselt ! [xh3o44U].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/1eed7a37-b462-403c-adf6-6120361b0c1b.mp4" - }, - { - "id": "1f1fd37b-55ec-48df-8971-91039028f1c1", - "created_date": "2024-07-25 07:29:45.937602", - "last_modified_date": "2024-07-25 07:29:45.937602", - "version": 0, - "url": "https://ge.xhamster.com/videos/colonized-4-xhTYd79", - "review": 0, - "should_download": 0, - "title": "Kolonisiert # 4 | xHamster", - "file_name": "Kolonisiert # 4 [xhTYd79].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1f1fd37b-55ec-48df-8971-91039028f1c1.mp4" - }, - { - "id": "1f3b17b6-08de-4ab8-80b1-435eb4524883", - "created_date": "2025-01-16 20:00:03.537011", - "last_modified_date": "2025-01-16 20:49:08.140000", - "version": 2, - "url": "https://ge.xhamster.com/videos/a-night-out-at-the-bar-bold-move-on-sexy-customer-xh0G8BP", - "review": 0, - "should_download": 0, - "title": "Eine nacht an der bar: Mutiger zug auf sexy kunden | xHamster", - "file_name": "Eine nacht an der bar\uff1a Mutiger zug auf sexy kunden [xh0G8BP].mp4", - "path": null, - "cloud_link": "/data/media/1f3b17b6-08de-4ab8-80b1-435eb4524883.mp4" - }, - { - "id": "1f781d06-ee8d-4251-be6a-734fbd837b61", - "created_date": "2024-08-16 11:12:03.493000", - "last_modified_date": "2024-10-21 16:26:18.292000", - "version": 1, - "url": "https://ge.xhamster.com/videos/stacy-open-her-legs-to-seduce-friend-son-near-his-step-mom-xhL1g83", - "review": 0, - "should_download": 0, - "title": "Stacy \u00f6ffnet ihre Beine, um den Sohn ihres Freundes nahe seiner Stiefmutter zu verf\u00fchren | xHamster", - "file_name": "Stacy \u00f6ffnet ihre Beine, um den Sohn ihres Freundes nahe seiner Stiefmutter zu verf\u00fchren [xhL1g83].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/1f781d06-ee8d-4251-be6a-734fbd837b61.mp4" - }, - { - "id": "1fa2a2fe-e1b9-41b7-8063-0ae95d98da01", - "created_date": "2024-07-25 07:29:46.725872", - "last_modified_date": "2024-07-25 07:29:46.725872", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-love-boat-1833824", - "review": 0, - "should_download": 0, - "title": "Das Liebesboot | xHamster", - "file_name": "Das Liebesboot [1833824].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1fa2a2fe-e1b9-41b7-8063-0ae95d98da01.mp4" - }, - { - "id": "1fd3cf92-ab69-4c52-b9fe-fef8c8aec0d9", - "created_date": "2024-07-25 07:29:47.235502", - "last_modified_date": "2024-07-25 07:29:47.235502", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-young-secretaries-1974-7224683", - "review": 0, - "should_download": 0, - "title": "Die jungen Sekret\u00e4rinnen (1974) | xHamster", - "file_name": "Die jungen Sekret\u00e4rinnen (1974) [7224683].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1fd3cf92-ab69-4c52-b9fe-fef8c8aec0d9.mp4" - }, - { - "id": "1fd576af-9d65-4f32-80e9-1b14d931ee41", - "created_date": "2024-07-25 07:29:46.698554", - "last_modified_date": "2024-07-25 07:29:46.698554", - "version": 0, - "url": "https://ge.xhamster.com/videos/gangbang-in-the-rain-233403", - "review": 0, - "should_download": 0, - "title": "Gangbang im Regen | xHamster", - "file_name": "Gangbang im Regen [233403].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1fd576af-9d65-4f32-80e9-1b14d931ee41.mp4" - }, - { - "id": "1fd7c41e-38ca-4a3b-8771-5c78b3768394", - "created_date": "2024-07-25 07:29:44.741121", - "last_modified_date": "2024-07-25 07:29:44.741121", - "version": 0, - "url": "https://ge.xhamster.com/videos/beach-orgy-13242586", - "review": 0, - "should_download": 0, - "title": "Strandorgie | xHamster", - "file_name": "Strandorgie [13242586].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1fd7c41e-38ca-4a3b-8771-5c78b3768394.mp4" - }, - { - "id": "1fe8b8a9-64b5-46a9-a28f-0444c291e26d", - "created_date": "2024-07-25 07:29:45.211396", - "last_modified_date": "2024-07-25 07:29:45.211396", - "version": 0, - "url": "https://ge.xhamster.com/videos/freeuse-fantasy-horny-stepbro-fucks-his-stepsisters-best-friend-hazel-heart-in-front-of-her-xhH3EUp", - "review": 0, - "should_download": 0, - "title": "Freeuse Fantasy, geiler Stiefbruder fickt Hazel Heart, den besten Freund seiner Stiefschwester, vor ihr | xHamster", - "file_name": "Freeuse Fantasy, geiler Stiefbruder fickt Hazel Heart, den besten Freund seiner Stiefschwester, vor ihr [xhH3EUp].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/1fe8b8a9-64b5-46a9-a28f-0444c291e26d.mp4" - }, - { - "id": "201b0ea8-52b7-44c5-83b3-527501bcde81", - "created_date": "2024-07-25 07:29:46.297761", - "last_modified_date": "2024-07-25 07:29:46.297761", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-rent-is-late-but-elles-ass-is-on-time-xhzSJml", - "review": 0, - "should_download": 0, - "title": "Die miete ist sp\u00e4t, aber Elles arsch ist zur zeit | xHamster", - "file_name": "Die miete ist sp\u00e4t, aber Elles arsch ist zur zeit [xhzSJml].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/201b0ea8-52b7-44c5-83b3-527501bcde81.mp4" - }, - { - "id": "2020c00c-bd67-4e1d-9d91-c147d25a5efa", - "created_date": "2024-07-25 07:29:47.892877", - "last_modified_date": "2024-07-25 07:29:47.892877", - "version": 0, - "url": "https://ge.xhamster.com/videos/schoolgirls-8-xhgbBQY", - "review": 0, - "should_download": 0, - "title": "Schulm\u00e4dchen 8 - Blutjunge Sch\u00fclerinnen ... Spritzig Verf\u00fchrt! | xHamster", - "file_name": "Schulm\u00e4dchen 8 - Blutjunge Sch\u00fclerinnen ... Spritzig Verf\u00fchrt! [xhgbBQY].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2020c00c-bd67-4e1d-9d91-c147d25a5efa.mp4" - }, - { - "id": "203a71c8-bdfd-4efe-b4b9-7c9f9428f932", - "created_date": "2024-07-25 07:29:44.902881", - "last_modified_date": "2024-07-25 07:29:44.902881", - "version": 0, - "url": "https://ge.xhamster.com/videos/outdoor-threesome-by-the-lake-8770332", - "review": 0, - "should_download": 0, - "title": "Outdoor-Dreier am See | xHamster", - "file_name": "Outdoor-Dreier am See [8770332].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/203a71c8-bdfd-4efe-b4b9-7c9f9428f932.mp4" - }, - { - "id": "205c35fb-688e-44d4-8550-84e3278d45bb", - "created_date": "2024-07-25 07:29:45.869249", - "last_modified_date": "2024-07-25 07:29:45.869249", - "version": 0, - "url": "https://ge.xhamster.com/videos/rk-prime-abella-danger-sean-lawless-her-ex-fucks-her-11577067", - "review": 0, - "should_download": 0, - "title": "Rk prime - Abella Danger Sean Lawless - ihr Ex fickt sie | xHamster", - "file_name": "Rk prime - Abella Danger Sean Lawless - ihr Ex fickt sie [11577067].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/205c35fb-688e-44d4-8550-84e3278d45bb.mp4" - }, - { - "id": "205e4eb0-a1a7-45f7-ad02-d25a287692c5", - "created_date": "2024-07-25 07:29:47.975713", - "last_modified_date": "2024-07-25 07:29:47.975713", - "version": 0, - "url": "https://ge.xhamster.com/videos/maedel-im-urlaub-beim-sonnenbaden-und-massieren-xhuN7OP", - "review": 0, - "should_download": 0, - "title": "Maedel Im Urlaub Beim Sonnenbaden Und Massieren: HD Porn c7 | xHamster", - "file_name": "Maedel im Urlaub beim sonnenbaden und massieren [xhuN7OP].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/205e4eb0-a1a7-45f7-ad02-d25a287692c5.mp4" - }, - { - "id": "20b95aa4-404b-40f6-bbd2-810d487e88c6", - "created_date": "2024-07-25 07:29:44.536383", - "last_modified_date": "2024-07-25 07:29:44.536383", - "version": 0, - "url": "https://ge.xhamster.com/videos/nudist-driver-takes-me-when-i-was-hitchhiking-outdoor-sex-xh3ucuQ", - "review": 0, - "should_download": 0, - "title": "FKK-Fahrer nimmt mich mit, als ich per Anhalter fuhr. Outdoor-Sex | xHamster", - "file_name": "FKK-Fahrer nimmt mich mit, als ich per Anhalter fuhr. Outdoor-Sex [xh3ucuQ].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/20b95aa4-404b-40f6-bbd2-810d487e88c6.mp4" - }, - { - "id": "20dced78-8d76-4e6b-9139-b65fb362f9d6", - "created_date": "2024-07-25 07:29:46.633229", - "last_modified_date": "2024-07-25 07:29:46.633229", - "version": 0, - "url": "https://ge.xhamster.com/videos/ive-seen-your-dick-twice-maybe-you-should-fuck-me-already-xhpJTq9", - "review": 0, - "should_download": 0, - "title": "Ich habe deinen Schwanz zweimal gesehen, vielleicht solltest du mich schon ficken? | xHamster", - "file_name": "Ich habe deinen Schwanz zweimal gesehen, vielleicht solltest du mich schon ficken\uff1f [xhpJTq9].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/20dced78-8d76-4e6b-9139-b65fb362f9d6.mp4" - }, - { - "id": "20e23a19-e71a-4100-8845-00534b825b5d", - "created_date": "2024-07-25 07:29:45.460706", - "last_modified_date": "2024-07-25 07:29:45.460706", - "version": 0, - "url": "https://ge.xhamster.com/videos/vintage-1979-crazy-orgy-xhPeRmI", - "review": 0, - "should_download": 0, - "title": "Vintage 1979 - verr\u00fcckte Orgie | xHamster", - "file_name": "Vintage 1979 - verr\u00fcckte Orgie [xhPeRmI].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/20e23a19-e71a-4100-8845-00534b825b5d.mp4" - }, - { - "id": "20e9ca38-cf93-4808-8267-9549754961f5", - "created_date": "2024-07-25 07:29:45.793252", - "last_modified_date": "2024-07-25 07:29:45.793252", - "version": 0, - "url": "https://ge.xhamster.com/videos/weltklasse-arsche-5-full-movie-xh0DpwR", - "review": 0, - "should_download": 0, - "title": "Weltklasse Arsche 5 Full Movie, Free Big Cock HD Porn 6e | xHamster", - "file_name": "Weltklasse Arsche 5 (Full Movie) [xh0DpwR].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/20e9ca38-cf93-4808-8267-9549754961f5.mp4" - }, - { - "id": "20f74382-b1c5-4562-b14c-b47f0accfab3", - "created_date": "2024-07-25 07:29:47.816828", - "last_modified_date": "2024-07-25 07:29:47.816828", - "version": 0, - "url": "https://ge.xhamster.com/videos/satisfaction-guaranteed-1976-10204054", - "review": 0, - "should_download": 0, - "title": "Zufriedenheit garantiert (1976) | xHamster", - "file_name": "Zufriedenheit garantiert (1976) [10204054].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/20f74382-b1c5-4562-b14c-b47f0accfab3.mp4" - }, - { - "id": "21e5842b-6ce2-41e5-a6cd-34b7354f2c07", - "created_date": "2025-01-17 16:34:31.698174", - "last_modified_date": "2025-01-17 16:34:31.698181", - "version": 0, - "url": "https://ge.xhamster.com/videos/a-slim-and-beautiful-german-chick-gets-gangbnaged-at-the-bar-xhnwUIG", - "review": 0, - "should_download": 0, - "title": "Ein schlankes und sch\u00f6nes deutsches k\u00fcken wird an der bar gangbang | xHamster", - "file_name": "Ein schlankes und sch\u00f6nes deutsches k\u00fcken wird an der bar gangbang [xhnwUIG].mp4", - "path": null, - "cloud_link": "/data/media/21e5842b-6ce2-41e5-a6cd-34b7354f2c07.mp4" - }, - { - "id": "220842be-5e43-4c3e-8ba3-8b204949188d", - "created_date": "2024-12-29 23:53:27.915735", - "last_modified_date": "2024-12-29 23:53:27.915735", - "version": 0, - "url": "https://ge.xhamster.com/videos/hottest-classics-hd-7-11427446", - "review": 0, - "should_download": 0, - "title": "Hei\u00dfeste Klassiker hd 7 | xHamster", - "file_name": "Hei\u00dfeste Klassiker hd 7 [11427446].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/220842be-5e43-4c3e-8ba3-8b204949188d.mp4" - }, - { - "id": "2210755d-a5a9-4533-946c-6682edafbcf6", - "created_date": "2024-07-25 07:29:44.367215", - "last_modified_date": "2024-07-25 07:29:44.367215", - "version": 0, - "url": "https://ge.xhamster.com/videos/fun-on-comstation-5932436", - "review": 0, - "should_download": 0, - "title": "Spa\u00df auf der Comstation | xHamster", - "file_name": "Spa\u00df auf der Comstation [5932436].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2210755d-a5a9-4533-946c-6682edafbcf6.mp4" - }, - { - "id": "221acfd1-4b7f-4c4a-aeb9-1f4492f6abc5", - "created_date": "2024-07-25 07:29:46.192543", - "last_modified_date": "2024-07-25 07:29:46.192543", - "version": 0, - "url": "https://ge.xhamster.com/videos/mommys-boy-naughty-milf-siri-dahls-caught-naked-in-the-kitchen-pervert-stepson-banged-her-hard-xhIoP39", - "review": 0, - "should_download": 0, - "title": "MAMAS JUNGE - die freche MILF Siri Dahl wird nackt in der K\u00fcche erwischt! Perverser stiefsohn hat sie hart geknallt! | xHamster", - "file_name": "MAMAS JUNGE - die freche MILF Siri Dahl wird nackt in der K\u00fcche erwischt! Perverser stiefsohn hat sie hart geknallt! [xhIoP39].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/221acfd1-4b7f-4c4a-aeb9-1f4492f6abc5.mp4" - }, - { - "id": "224a5257-194e-49ec-9f52-6c27487de93e", - "created_date": "2024-07-25 07:29:47.540061", - "last_modified_date": "2024-07-25 07:29:47.540061", - "version": 0, - "url": "https://ge.xhamster.com/videos/vixen-she-couldn-t-resist-a-naughty-vacation-with-stranger-xhMFSdx", - "review": 0, - "should_download": 0, - "title": "Vixen, sie konnte einem frechen Urlaub mit Fremdem nicht widerstehen | xHamster", - "file_name": "Vixen, sie konnte einem frechen Urlaub mit Fremdem nicht widerstehen [xhMFSdx].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/224a5257-194e-49ec-9f52-6c27487de93e.mp4" - }, - { - "id": "225d6949-f033-4a57-a11d-7b6fe31dc8a6", - "created_date": "2024-07-25 07:29:47.314648", - "last_modified_date": "2024-07-25 07:29:47.314648", - "version": 0, - "url": "https://ge.xhamster.com/videos/fickektesse-11855624", - "review": 0, - "should_download": 0, - "title": "Fickektesse: Free Big Tits Porn Video 2f | xHamster", - "file_name": "fickektesse [11855624].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/225d6949-f033-4a57-a11d-7b6fe31dc8a6.mp4" - }, - { - "id": "226d2e5e-8748-4447-97eb-0ddb2065e4cb", - "created_date": "2024-07-25 07:29:44.277816", - "last_modified_date": "2024-07-25 07:29:44.277816", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-group-sex-1995-11042955", - "review": 0, - "should_download": 0, - "title": "Familien-Gruppensex (1995) | xHamster", - "file_name": "Familien-Gruppensex (1995) [11042955].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/226d2e5e-8748-4447-97eb-0ddb2065e4cb.mp4" - }, - { - "id": "2276ddac-1c0c-4b14-8620-d9ed11ecd00c", - "created_date": "2024-07-25 07:29:47.096888", - "last_modified_date": "2024-07-25 07:29:47.096888", - "version": 0, - "url": "https://ge.xhamster.com/videos/do-you-like-anal-stepsis-analy-fucked-by-hot-stepbro-xh1iSth", - "review": 0, - "should_download": 0, - "title": "\u201eMagst du Analsex?\u201c Stiefschwester wird von hei\u00dfem Stiefbruder anal gefickt | xHamster", - "file_name": "\u201eMagst du Analsex\uff1f\u201c Stiefschwester wird von hei\u00dfem Stiefbruder anal gefickt [xh1iSth].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2276ddac-1c0c-4b14-8620-d9ed11ecd00c.mp4" - }, - { - "id": "2294ddfc-d2e3-48e3-b582-b33977c6a69d", - "created_date": "2024-07-25 07:29:45.543764", - "last_modified_date": "2024-07-25 07:29:45.543764", - "version": 0, - "url": "https://ge.xhamster.com/videos/schluckspechte-full-german-movie-xhfAgzQ", - "review": 0, - "should_download": 0, - "title": "Schluckspechte- Full German Movie, Free Porn b2 | xHamster", - "file_name": "Schluckspechte- full german movie [xhfAgzQ].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/2294ddfc-d2e3-48e3-b582-b33977c6a69d.mp4" - }, - { - "id": "22f4cd27-ddb5-4ef2-865b-68815f92fed2", - "created_date": "2024-07-25 07:29:47.903315", - "last_modified_date": "2024-07-25 07:29:47.903315", - "version": 0, - "url": "https://ge.xhamster.com/videos/pure-taboo-2-step-brothers-dp-their-step-mom-xhCUWQx", - "review": 0, - "should_download": 0, - "title": "Pures Tabu, 2 Stiefbr\u00fcder doppelpenetrieren ihre Stiefmutter | xHamster", - "file_name": "Pures Tabu, 2 Stiefbr\u00fcder doppelpenetrieren ihre Stiefmutter [xhCUWQx].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/22f4cd27-ddb5-4ef2-865b-68815f92fed2.mp4" - }, - { - "id": "232bc320-aab1-4fa4-8688-53ab5c3f6da7", - "created_date": "2024-07-25 07:29:46.896280", - "last_modified_date": "2024-07-25 07:29:46.896280", - "version": 0, - "url": "https://ge.xhamster.com/videos/boat-group-sex-games-xhX5aeg", - "review": 0, - "should_download": 0, - "title": "Boot Gruppensex-Spiele | xHamster", - "file_name": "Boot Gruppensex-Spiele [xhX5aeg].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/232bc320-aab1-4fa4-8688-53ab5c3f6da7.mp4" - }, - { - "id": "2388019e-a4ce-48fd-8e8a-0abb21d38f1c", - "created_date": "2024-07-25 07:29:46.799056", - "last_modified_date": "2024-07-25 07:29:46.799056", - "version": 0, - "url": "https://ge.xhamster.com/videos/new-stepmom-caught-stepson-masturbating-on-cam-xhhcYiq", - "review": 0, - "should_download": 0, - "title": "Neue stiefmutter erwischt stiefsohn beim masturbieren vor der kamera | xHamster", - "file_name": "Neue stiefmutter erwischt stiefsohn beim masturbieren vor der kamera [xhhcYiq].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2388019e-a4ce-48fd-8e8a-0abb21d38f1c.mp4" - }, - { - "id": "23912fb7-f0d4-4a63-b378-fcfb23dee2e2", - "created_date": "2024-09-05 20:03:45.646354", - "last_modified_date": "2024-10-21 16:26:24.771000", - "version": 1, - "url": "https://ge.xhamster.com/videos/chubby-dutch-fucked-on-the-beach-6393203", - "review": 0, - "should_download": 0, - "title": "Mollige Holl\u00e4nderin am Strand gefickt | xHamster", - "file_name": "Mollige Holl\u00e4nderin am Strand gefickt [6393203].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/23912fb7-f0d4-4a63-b378-fcfb23dee2e2.mp4" - }, - { - "id": "2393558b-9373-4fdf-97bf-62e82d7d7b43", - "created_date": "2024-07-25 07:29:46.358094", - "last_modified_date": "2024-07-25 07:29:46.358094", - "version": 0, - "url": "https://ge.xhamster.com/videos/german-brother-and-sister-in-bathroom-10805663", - "review": 0, - "should_download": 0, - "title": "Deutscher Bruder und Schwester im Badezimmer | xHamster", - "file_name": "Deutscher Bruder und Schwester im Badezimmer [10805663].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2393558b-9373-4fdf-97bf-62e82d7d7b43.mp4" - }, - { - "id": "23ca75c8-44f3-456e-abc4-79c0df5d1e3a", - "created_date": "2024-07-25 07:29:47.149097", - "last_modified_date": "2024-07-25 07:29:47.149097", - "version": 0, - "url": "https://ge.xhamster.com/videos/sex-ausflug-der-13-a-2002-13901379", - "review": 0, - "should_download": 0, - "title": "Sex-ausflug Der 13-a 2002, Free European Porn 79 | xHamster", - "file_name": "Sex-Ausflug der 13-A (2002) [13901379].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/23ca75c8-44f3-456e-abc4-79c0df5d1e3a.mp4" - }, - { - "id": "23f2a86e-702f-4b84-a42e-ca0435118b95", - "created_date": "2024-07-25 07:29:44.234351", - "last_modified_date": "2024-07-25 07:29:44.234351", - "version": 0, - "url": "https://ge.xhamster.com/videos/kinky-family-lacy-lennon-my-stepsis-took-my-virginity-13129437", - "review": 0, - "should_download": 0, - "title": "Versaute Familie - Lacy Lennon - meine Stiefschwester hat meine Jungfr\u00e4ulichkeit genommen | xHamster", - "file_name": "Versaute Familie - Lacy Lennon - meine Stiefschwester hat meine Jungfr\u00e4ulichkeit genommen [13129437].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/23f2a86e-702f-4b84-a42e-ca0435118b95.mp4" - }, - { - "id": "2417aa9c-9374-4d3c-952e-cd923ae546f6", - "created_date": "2024-07-25 07:29:45.502079", - "last_modified_date": "2024-07-25 07:29:45.502079", - "version": 0, - "url": "https://ge.xhamster.com/videos/leonora-st-john-british-retro-anal-from-the-1990s-12741986", - "review": 0, - "should_download": 0, - "title": "Leonora St John - britischer Retro-Anal aus den 1990er Jahren | xHamster", - "file_name": "Leonora St John - britischer Retro-Anal aus den 1990er Jahren [12741986].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2417aa9c-9374-4d3c-952e-cd923ae546f6.mp4" - }, - { - "id": "24384b80-08e2-46d9-8925-1c0ece6544cb", - "created_date": "2024-07-25 07:29:47.574262", - "last_modified_date": "2024-07-25 07:29:47.574262", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepbro-walks-in-on-step-sister-in-the-bathtub-masturbating-with-a-big-dildo-sislovesme-xhWB4Bt", - "review": 0, - "should_download": 0, - "title": "Stiefbruder ertritt stiefschwester in der badewanne und masturbiert mit einem gro\u00dfen dildo - sislovesMe | xHamster", - "file_name": "Stiefbruder ertritt stiefschwester in der badewanne und masturbiert mit einem gro\u00dfen dildo - sislovesMe [xhWB4Bt].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/24384b80-08e2-46d9-8925-1c0ece6544cb.mp4" - }, - { - "id": "243fc016-769f-41fb-b965-134adfe468bf", - "created_date": "2024-07-25 07:29:46.813858", - "last_modified_date": "2024-07-25 07:29:46.813858", - "version": 0, - "url": "https://ge.xhamster.com/videos/v-b-11156751", - "review": 0, - "should_download": 0, - "title": "V B: Free Porn Video 76 | xHamster", - "file_name": "V.B. [11156751].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/243fc016-769f-41fb-b965-134adfe468bf.mp4" - }, - { - "id": "248a35ad-729c-485a-bfa3-22b9467a4b25", - "created_date": "2024-07-25 07:29:45.900375", - "last_modified_date": "2024-07-25 07:29:45.900375", - "version": 0, - "url": "https://ge.xhamster.com/videos/corporate-assets-1985-7355919", - "review": 0, - "should_download": 0, - "title": "Unternehmensverm\u00f6gen - 1985 | xHamster", - "file_name": "Unternehmensverm\u00f6gen - 1985 [7355919].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/248a35ad-729c-485a-bfa3-22b9467a4b25.mp4" - }, - { - "id": "2497f0e7-966c-4a76-9a41-b8bd0878365d", - "created_date": "2024-07-25 07:29:47.012200", - "last_modified_date": "2024-07-25 07:29:47.012200", - "version": 0, - "url": "https://ge.xhamster.com/videos/usa-orgies-corporation-vol-03-xhv1eUS", - "review": 0, - "should_download": 0, - "title": "USA Orgien Corporation !!!! - Vol # 03 | xHamster", - "file_name": "USA Orgien Corporation !!!! - Vol # 03 [xhv1eUS].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2497f0e7-966c-4a76-9a41-b8bd0878365d.mp4" - }, - { - "id": "24f5cb78-5882-48f6-a9dc-789b03ec1e17", - "created_date": "2024-07-25 07:29:44.801593", - "last_modified_date": "2024-07-25 07:29:44.801593", - "version": 0, - "url": "https://ge.xhamster.com/videos/blonde-teen-step-sister-gets-a-public-creampie-on-a-boat-12114195", - "review": 0, - "should_download": 0, - "title": "Blondes Teen Stiefschwester bekommt einen \u00f6ffentlichen Creampie auf einem Boot | xHamster", - "file_name": "Blondes Teen Stiefschwester bekommt einen \u00f6ffentlichen Creampie auf einem Boot [12114195].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/24f5cb78-5882-48f6-a9dc-789b03ec1e17.mp4" - }, - { - "id": "24f6a139-1eb8-456f-8d41-8425b912362e", - "created_date": "2024-07-25 07:29:44.477016", - "last_modified_date": "2024-07-25 07:29:44.477016", - "version": 0, - "url": "https://ge.xhamster.com/videos/carolina-sweets-attracted-to-her-step-dads-old-friend-9209254", - "review": 0, - "should_download": 0, - "title": "Carolina Sweets wurde von der alten Freundin ihres Stiefvaters angezogen | xHamster", - "file_name": "Carolina Sweets wurde von der alten Freundin ihres Stiefvaters angezogen [9209254].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/24f6a139-1eb8-456f-8d41-8425b912362e.mp4" - }, - { - "id": "2623d347-2989-405d-bc73-a44e6f59a4f2", - "created_date": "2024-07-25 07:29:45.944756", - "last_modified_date": "2024-07-25 07:29:45.944756", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-old-man-fucked-a-young-librarian-9650252", - "review": 0, - "should_download": 0, - "title": "Der alte Mann fickte eine junge Bibliothekarin | xHamster", - "file_name": "Der alte Mann fickte eine junge Bibliothekarin [9650252].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2623d347-2989-405d-bc73-a44e6f59a4f2.mp4" - }, - { - "id": "263237bf-ae86-4c8a-98bc-66897382652e", - "created_date": "2024-07-25 07:29:47.228106", - "last_modified_date": "2024-07-25 07:29:47.228106", - "version": 0, - "url": "https://ge.xhamster.com/videos/adult-time-riley-reids-insane-threesome-with-her-stepdad-teen-bff-janice-griffith-part-1-and-2-xhzJOOG", - "review": 0, - "should_download": 0, - "title": "ERWACHSENENZEIT - Riley Reids WAHNSINNIGER DREIER mit ihrem stiefvater & teen BFF Janice Griffith! TEIL 1 und 2 | xHamster", - "file_name": "ERWACHSENENZEIT - Riley Reids WAHNSINNIGER DREIER mit ihrem stiefvater & teen BFF Janice Griffith! TEIL 1 und 2 [xhzJOOG].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/263237bf-ae86-4c8a-98bc-66897382652e.mp4" - }, - { - "id": "266853c1-caed-42d7-853e-183fb23c6014", - "created_date": "2024-07-25 07:29:46.580498", - "last_modified_date": "2024-07-25 07:29:46.580498", - "version": 0, - "url": "https://ge.xhamster.com/videos/you-were-gonna-fuck-my-friend-s15-e6-xhaitwW", - "review": 0, - "should_download": 0, - "title": "Du wirst meinen freund ficken - S15: e6 | xHamster", - "file_name": "Du wirst meinen freund ficken - S15\uff1a e6 [xhaitwW].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/266853c1-caed-42d7-853e-183fb23c6014.mp4" - }, - { - "id": "26b04ee7-3fcb-4be6-8c1f-036605236bcc", - "created_date": "2024-07-25 07:29:44.377828", - "last_modified_date": "2024-07-25 07:29:44.377828", - "version": 0, - "url": "https://ge.xhamster.com/videos/geschichte-der-o-episode-1-6273303", - "review": 0, - "should_download": 0, - "title": "Geschichte Der O - Episode 1, Free German Porn aa | xHamster", - "file_name": "Geschichte der O - Episode 1 [6273303].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/26b04ee7-3fcb-4be6-8c1f-036605236bcc.mp4" - }, - { - "id": "26c47473-2953-4315-a180-fff574db53ce", - "created_date": "2024-12-29 23:53:27.918420", - "last_modified_date": "2024-12-29 23:53:27.918420", - "version": 0, - "url": "https://ge.xhamster.com/videos/they-love-kissing-each-other-with-my-cum-on-their-lips-xhTGRvJb", - "review": 0, - "should_download": 0, - "title": "Sie lieben es, sich gegenseitig mit meinem sperma auf ihren lippen zu k\u00fcssen | xHamster", - "file_name": "Sie lieben es, sich gegenseitig mit meinem sperma auf ihren lippen zu k\u00fcssen [xhTGRvJb].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/26c47473-2953-4315-a180-fff574db53ce.mp4" - }, - { - "id": "271219bf-b1d4-4828-bea8-cf1a9db3e81b", - "created_date": "2024-07-25 07:29:47.450783", - "last_modified_date": "2024-07-25 07:29:47.450783", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-swinger-neighbors-make-noises-comp-xh7Tkrr", - "review": 0, - "should_download": 0, - "title": "Meine Swinger-Nachbarn machen Ger\u00e4usche | xHamster", - "file_name": "Meine Swinger-Nachbarn machen Ger\u00e4usche [xh7Tkrr].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/271219bf-b1d4-4828-bea8-cf1a9db3e81b.mp4" - }, - { - "id": "274bbe8a-819a-412b-89ef-cac2c692b18e", - "created_date": "2024-07-25 07:29:45.778732", - "last_modified_date": "2024-07-25 07:29:45.778732", - "version": 0, - "url": "https://ge.xhamster.com/videos/like-mother-like-daughter-1973-restored-7766219", - "review": 0, - "should_download": 0, - "title": "Wie Mutter, wie Tochter - 1973 (restauriert) | xHamster", - "file_name": "Wie Mutter, wie Tochter - 1973 (restauriert) [7766219].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/274bbe8a-819a-412b-89ef-cac2c692b18e.mp4" - }, - { - "id": "2765fb44-c458-461a-959b-80b2531561b8", - "created_date": "2024-07-25 07:29:47.567314", - "last_modified_date": "2024-07-25 07:29:47.567314", - "version": 0, - "url": "https://ge.xhamster.com/videos/foursome-with-dp-3882121", - "review": 0, - "should_download": 0, - "title": "Vierer mit Doppelpenetration | xHamster", - "file_name": "Vierer mit Doppelpenetration [3882121].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2765fb44-c458-461a-959b-80b2531561b8.mp4" - }, - { - "id": "28319815-217e-449c-9d98-82574e534e9d", - "created_date": "2024-07-25 07:29:45.290059", - "last_modified_date": "2024-09-06 09:41:02.745000", - "version": 1, - "url": "https://ge.xhamster.com/videos/lusty-boarding-school-xhGBNwZ", - "review": 0, - "should_download": 0, - "title": "Lustvolles Internat", - "file_name": "Lustvolles Internat [xhGBNwZ].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/28319815-217e-449c-9d98-82574e534e9d.mp4" - }, - { - "id": "287256a2-47c2-4170-90eb-c9a6bfc4973f", - "created_date": "2024-07-25 07:29:44.824928", - "last_modified_date": "2024-07-25 07:29:44.824928", - "version": 0, - "url": "https://ge.xhamster.com/videos/memoiren-der-lust-1979-6285457", - "review": 0, - "should_download": 0, - "title": "Memoiren Der Lust 1979, Free Orgy Porn Video f3 | xHamster", - "file_name": "Memoiren der Lust (1979) [6285457].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/287256a2-47c2-4170-90eb-c9a6bfc4973f.mp4" - }, - { - "id": "28b9c9a9-45bf-4128-8533-7f2c50026fcd", - "created_date": "2024-07-25 07:29:47.794082", - "last_modified_date": "2024-07-25 07:29:47.794082", - "version": 0, - "url": "https://ge.xhamster.com/videos/young-sluts-in-search-of-love-full-movie-xhtYq8y", - "review": 0, - "should_download": 0, - "title": "Junge Schlampen auf der Suche nach Liebe (kompletter Film) | xHamster", - "file_name": "Junge Schlampen auf der Suche nach Liebe (kompletter Film) [xhtYq8y].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/28b9c9a9-45bf-4128-8533-7f2c50026fcd.mp4" - }, - { - "id": "28f45aa0-5177-4419-98d1-bfd2617201da", - "created_date": "2024-07-25 07:29:47.620909", - "last_modified_date": "2024-07-25 07:29:47.620909", - "version": 0, - "url": "https://ge.xhamster.com/videos/busty-redhead-taxi-cutie-assfucked-by-driver-5448301", - "review": 0, - "should_download": 0, - "title": "Vollbusige rothaarige Taxi-S\u00fc\u00dfe vom Fahrer arschgefickt | xHamster", - "file_name": "Vollbusige rothaarige Taxi-S\u00fc\u00dfe vom Fahrer arschgefickt [5448301].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/28f45aa0-5177-4419-98d1-bfd2617201da.mp4" - }, - { - "id": "293e5d1a-260d-42fa-ae02-54e20f6d866f", - "created_date": "2024-07-25 07:29:47.105424", - "last_modified_date": "2024-07-25 07:29:47.105424", - "version": 0, - "url": "https://ge.xhamster.com/videos/cant-you-see-im-busy-wait-do-you-have-a-boner-britt-blair-asks-stepbro-s28-e3-xhWtQpv", - "review": 0, - "should_download": 0, - "title": "\"Kannst du nicht sehen, dass ich besch\u00e4ftigt bin? Warte, hast du eine Latte ?!\" Britt Blair fragt Stiefbruder -s28: e3 | xHamster", - "file_name": "\uff02Kannst du nicht sehen, dass ich besch\u00e4ftigt bin\uff1f Warte, hast du eine Latte \uff1f!\uff02 Britt Blair fragt Stiefbruder -s28\uff1a e3 [xhWtQpv].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/293e5d1a-260d-42fa-ae02-54e20f6d866f.mp4" - }, - { - "id": "29900405-ec86-4675-9524-fffb200940cb", - "created_date": "2024-07-25 07:29:44.808581", - "last_modified_date": "2024-07-25 07:29:44.808581", - "version": 0, - "url": "https://ge.xhamster.com/videos/gangbanged-wife-xhKL12h", - "review": 0, - "should_download": 0, - "title": "GANGBANGED WIFE | xHamster", - "file_name": "GANGBANGED WIFE [xhKL12h].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/29900405-ec86-4675-9524-fffb200940cb.mp4" - }, - { - "id": "29d471ea-4fbf-4860-adc1-47291f74e028", - "created_date": "2024-10-07 20:47:56.428026", - "last_modified_date": "2024-10-21 16:26:31.598000", - "version": 1, - "url": "https://ge.xhamster.com/videos/18-videoz-partying-drinking-and-fucking-6951146", - "review": 0, - "should_download": 0, - "title": "18 Videoz - feiern, trinken und ficken | xHamster", - "file_name": "18 Videoz - feiern, trinken und ficken [6951146].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/29d471ea-4fbf-4860-adc1-47291f74e028.mp4" - }, - { - "id": "2a7247ab-27b8-46e4-b951-29ed896e2342", - "created_date": "2024-07-25 07:29:46.269438", - "last_modified_date": "2024-07-25 07:29:46.269438", - "version": 0, - "url": "https://ge.xhamster.com/videos/dienst-imhausechicfick-xh6TBnP", - "review": 0, - "should_download": 0, - "title": "Dienst Imhausechicfick, Free Retro Porn Video ed | xHamster", - "file_name": "Dienst imHauseChicfick [xh6TBnP].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2a7247ab-27b8-46e4-b951-29ed896e2342.mp4" - }, - { - "id": "2ad403b9-c2b9-4f40-8dc3-4eacda37a5bd", - "created_date": "2024-07-25 07:29:44.805011", - "last_modified_date": "2024-07-25 07:29:44.805011", - "version": 0, - "url": "https://ge.xhamster.com/videos/girl-double-penetrated-while-boat-trip-2674786", - "review": 0, - "should_download": 0, - "title": "M\u00e4dchen doppelt penetriert w\u00e4hrend Bootsfahrt | xHamster", - "file_name": "M\u00e4dchen doppelt penetriert w\u00e4hrend Bootsfahrt [2674786].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2ad403b9-c2b9-4f40-8dc3-4eacda37a5bd.mp4" - }, - { - "id": "2b1d148f-1e0f-48d6-aecf-6af31a62ce90", - "created_date": "2024-07-25 07:29:45.161561", - "last_modified_date": "2024-07-25 07:29:45.161561", - "version": 0, - "url": "https://ge.xhamster.com/videos/horny-female-teacher-has-threesome-with-schoolgirl-and-boy-student-in-class-xhyzF3P", - "review": 0, - "should_download": 0, - "title": "Geile lehrerin hat dreier mit schulm\u00e4dchen und sch\u00fcler in der klasse | xHamster", - "file_name": "Geile lehrerin hat dreier mit schulm\u00e4dchen und sch\u00fcler in der klasse [xhyzF3P].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2b1d148f-1e0f-48d6-aecf-6af31a62ce90.mp4" - }, - { - "id": "2b4e6588-c87f-47ad-8e36-ed525054ab92", - "created_date": "2024-07-25 07:29:45.325877", - "last_modified_date": "2024-07-25 07:29:45.325877", - "version": 0, - "url": "https://ge.xhamster.com/videos/mother-step-daughter-learn-to-share-kara-lee-dava-foxx-14933477", - "review": 0, - "should_download": 0, - "title": "Mutter und Stieftochter lernen, zu teilen - Kara Lee & Dava Foxx | xHamster", - "file_name": "Mutter und Stieftochter lernen, zu teilen - Kara Lee & Dava Foxx [14933477].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/2b4e6588-c87f-47ad-8e36-ed525054ab92.mp4" - }, - { - "id": "2b79e015-e533-4fbb-b48d-69d5aedcac97", - "created_date": "2024-07-25 07:29:45.715342", - "last_modified_date": "2024-07-25 07:29:45.715342", - "version": 0, - "url": "https://ge.xhamster.com/videos/boarding-school-1970-10269405", - "review": 0, - "should_download": 0, - "title": "Internat (1970) | xHamster", - "file_name": "Internat (1970) [10269405].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2b79e015-e533-4fbb-b48d-69d5aedcac97.mp4" - }, - { - "id": "2c97b96a-d623-4775-b994-d8f80325123c", - "created_date": "2024-07-25 07:29:46.315242", - "last_modified_date": "2024-07-25 07:29:46.315242", - "version": 0, - "url": "https://ge.xhamster.com/videos/pool-party-orgy-with-a-bunch-of-horny-teens-bitches-11135779", - "review": 0, - "should_download": 0, - "title": "Poolparty-Orgie mit ein paar geilen Teenager-Schlampen | xHamster", - "file_name": "Poolparty-Orgie mit ein paar geilen Teenager-Schlampen [11135779].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2c97b96a-d623-4775-b994-d8f80325123c.mp4" - }, - { - "id": "2c993c58-dcb5-447e-ba38-5e05f5846525", - "created_date": "2024-07-25 07:29:44.594440", - "last_modified_date": "2024-07-25 07:29:44.594440", - "version": 0, - "url": "https://ge.xhamster.com/videos/first-sexual-experience-at-the-lake-2668426", - "review": 0, - "should_download": 0, - "title": "Erste sexuelle Erfahrung am See | xHamster", - "file_name": "Erste sexuelle Erfahrung am See [2668426].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2c993c58-dcb5-447e-ba38-5e05f5846525.mp4" - }, - { - "id": "2ca47897-9822-416f-a803-ef1c6bcefec2", - "created_date": "2024-07-25 07:29:44.946276", - "last_modified_date": "2025-01-03 11:56:22.273000", - "version": 1, - "url": "https://ge.xhamster.com/videos/ill-show-you-my-pussy-if-you-show-me-your-dick-lila-love-dares-stepbro-s26-e12-xhWyjzC", - "review": 0, - "should_download": 0, - "title": "\"Ich zeige dir meine Muschi, wenn .. du mir deinen Schwanz zeigst\" lila Liebe wagt Stiefbruder - s26: e12 | xHamster", - "file_name": "\uff02Ich zeige dir meine Muschi, wenn .. du mir deinen Schwanz zeigst\uff02 lila Liebe wagt Stiefbruder - s26\uff1a e12 [xhWyjzC].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2ca47897-9822-416f-a803-ef1c6bcefec2.mp4" - }, - { - "id": "2cc7c79a-4e48-4452-b2c6-03ffd9a64b3a", - "created_date": "2024-07-25 07:29:45.685134", - "last_modified_date": "2024-07-25 07:29:45.685134", - "version": 0, - "url": "https://ge.xhamster.com/videos/couples-play-spin-the-bottle-but-it-turns-into-fuck-the-neighbors-xh1CNyO", - "review": 0, - "should_download": 0, - "title": "Paare spielen, drehen die Flasche, aber es wird zu einem Fick der Nachbarn | xHamster", - "file_name": "Paare spielen, drehen die Flasche, aber es wird zu einem Fick der Nachbarn [xh1CNyO].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/2cc7c79a-4e48-4452-b2c6-03ffd9a64b3a.mp4" - }, - { - "id": "2ccdaf98-c4ed-44ad-bd49-28c8c89f309d", - "created_date": "2024-07-25 07:29:45.766806", - "last_modified_date": "2024-07-25 07:29:45.766806", - "version": 0, - "url": "https://ge.xhamster.com/videos/sniffing-the-babysitters-dirty-panties-leads-to-cheating-11394407", - "review": 0, - "should_download": 0, - "title": "Das schmutzige H\u00f6schen des Babysitters zu schn\u00fcffeln, f\u00fchrt zum Betr\u00fcgen! | xHamster", - "file_name": "Das schmutzige H\u00f6schen des Babysitters zu schn\u00fcffeln, f\u00fchrt zum Betr\u00fcgen! [11394407].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2ccdaf98-c4ed-44ad-bd49-28c8c89f309d.mp4" - }, - { - "id": "2cf47c13-3808-4520-a07a-ddad147b1afe", - "created_date": "2024-07-25 07:29:46.133668", - "last_modified_date": "2024-07-25 07:29:46.133668", - "version": 0, - "url": "https://ge.xhamster.com/videos/aerobic-sex-im-fickness-center-1988-german-dvd-rip-full-xhWRXlQ", - "review": 0, - "should_download": 0, - "title": "Aerobes Sex im Fickness Center (1988, deutsch, DVD Rip, voll) | xHamster", - "file_name": "Aerobes Sex im Fickness Center (1988, deutsch, DVD Rip, voll) [xhWRXlQ].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2cf47c13-3808-4520-a07a-ddad147b1afe.mp4" - }, - { - "id": "2d185929-bb75-40ae-8a14-cc8e9c76b0a8", - "created_date": "2024-07-25 07:29:44.522554", - "last_modified_date": "2024-07-25 07:29:44.522554", - "version": 0, - "url": "https://ge.xhamster.com/videos/american-college-xxx-the-originalin-hd-story-n-22-xhZhoI7", - "review": 0, - "should_download": 0, - "title": "Amerikanisches College xxx !!! - (das Original in HD) - Geschichte n. # 22 | xHamster", - "file_name": "Amerikanisches College xxx !!! - (das Original in HD) - Geschichte n. # 22 [xhZhoI7].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2d185929-bb75-40ae-8a14-cc8e9c76b0a8.mp4" - }, - { - "id": "2d1c3649-38a5-4320-8898-6c14cb0b67de", - "created_date": "2024-07-25 07:29:46.158584", - "last_modified_date": "2024-07-25 07:29:46.158584", - "version": 0, - "url": "https://ge.xhamster.com/videos/two-cocks-for-a-french-milf-in-heat-shes-so-horny-2317966", - "review": 0, - "should_download": 0, - "title": "Zwei Schw\u00e4nze f\u00fcr eine franz\u00f6sische MILF, die hei\u00df ist, sie ist so geil! | xHamster", - "file_name": "Zwei Schw\u00e4nze f\u00fcr eine franz\u00f6sische MILF, die hei\u00df ist, sie ist so geil! [2317966].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2d1c3649-38a5-4320-8898-6c14cb0b67de.mp4" - }, - { - "id": "2d2a8e85-0567-4798-b3a1-664ef7e96ec5", - "created_date": "2024-11-10 16:53:33.494244", - "last_modified_date": "2024-11-10 16:53:33.494244", - "version": 0, - "url": "https://ge.xhamster.com/videos/summer-vacation-family-hardcore-fucking-in-a-nice-villa-film-13823722", - "review": 0, - "should_download": 0, - "title": "Sommerferien Familien-Hardcore-Ficken in einem sch\u00f6nen Villenfilm | xHamster", - "file_name": "Sommerferien Familien-Hardcore-Ficken in einem sch\u00f6nen Villenfilm [13823722].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/2d2a8e85-0567-4798-b3a1-664ef7e96ec5.mp4" - }, - { - "id": "2d41e9bd-e5e0-4d31-9240-29d7c751e230", - "created_date": "2024-07-25 07:29:46.603654", - "last_modified_date": "2024-07-25 07:29:46.603654", - "version": 0, - "url": "https://ge.xhamster.com/videos/18-and-confused-1-xhkzIdN", - "review": 0, - "should_download": 0, - "title": "18 und verwirrt # 1 | xHamster", - "file_name": "18 und verwirrt # 1 [xhkzIdN].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2d41e9bd-e5e0-4d31-9240-29d7c751e230.mp4" - }, - { - "id": "2d66b335-4284-4ac9-95c3-1857393f476e", - "created_date": "2024-07-25 07:29:45.301022", - "last_modified_date": "2024-07-25 07:29:45.301022", - "version": 0, - "url": "https://ge.xhamster.com/videos/sweetsinner-step-siblings-unleash-pent-up-sexual-frustration-11856436", - "review": 0, - "should_download": 0, - "title": "Sweetsinner, Stiefgeschwister, entfesselt sexuelle Frustration | xHamster", - "file_name": "Sweetsinner, Stiefgeschwister, entfesselt sexuelle Frustration [11856436].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2d66b335-4284-4ac9-95c3-1857393f476e.mp4" - }, - { - "id": "2d7bad5b-1dcb-46ff-9e30-fa418b45d55a", - "created_date": "2024-07-25 07:29:44.510746", - "last_modified_date": "2024-07-25 07:29:44.510746", - "version": 0, - "url": "https://ge.xhamster.com/videos/die-geile-professorin-1976-13453153", - "review": 0, - "should_download": 0, - "title": "Die Geile Professorin 1976, Free European Porn b8 | xHamster", - "file_name": "Die Geile Professorin (1976) [13453153].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2d7bad5b-1dcb-46ff-9e30-fa418b45d55a.mp4" - }, - { - "id": "2dcd7d04-7861-4586-8d06-68c2396378ae", - "created_date": "2024-07-25 07:29:45.429862", - "last_modified_date": "2024-07-25 07:29:45.429862", - "version": 0, - "url": "https://ge.xhamster.com/videos/cartoon-porn-from-cartoonvalley-part-1-903113", - "review": 0, - "should_download": 0, - "title": "Cartoon-Porno aus Cartoonvalley Teil 1 | xHamster", - "file_name": "Cartoon-Porno aus Cartoonvalley Teil 1 [903113].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2dcd7d04-7861-4586-8d06-68c2396378ae.mp4" - }, - { - "id": "2e6db25c-2fdd-420c-8d63-6a101ef70bae", - "created_date": "2024-07-25 07:29:47.990030", - "last_modified_date": "2024-07-25 07:29:47.990030", - "version": 0, - "url": "https://ge.xhamster.com/videos/schone-bescherung-xhVNYIz", - "review": 0, - "should_download": 0, - "title": "Sch\u00f6ne \u00dcberraschung | xHamster", - "file_name": "Sch\u00f6ne \u00dcberraschung [xhVNYIz].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2e6db25c-2fdd-420c-8d63-6a101ef70bae.mp4" - }, - { - "id": "2e980db1-a41f-45d9-8eec-fa4e9394d48f", - "created_date": "2024-07-25 07:29:44.918047", - "last_modified_date": "2024-07-25 07:29:44.918047", - "version": 0, - "url": "https://ge.xhamster.com/videos/naughty-girls-5520628", - "review": 0, - "should_download": 0, - "title": "Freches M\u00e4dchen | xHamster", - "file_name": "Freches M\u00e4dchen [5520628].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/2e980db1-a41f-45d9-8eec-fa4e9394d48f.mp4" - }, - { - "id": "2f1dfa43-d8c8-4955-a683-26221a91bb05", - "created_date": "2024-07-25 07:29:47.859123", - "last_modified_date": "2024-07-25 07:29:47.859123", - "version": 0, - "url": "https://ge.xhamster.com/videos/when-your-boss-invites-you-to-a-gangbang-and-the-husband-doesnt-know-about-it-xhcy4QQ", - "review": 0, - "should_download": 0, - "title": "Wenn dein Chef zum Gangbang einl\u00e4dt und der Ehemann wei\u00df davon nichts | xHamster", - "file_name": "Wenn dein Chef zum Gangbang einl\u00e4dt und der Ehemann wei\u00df davon nichts [xhcy4QQ].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2f1dfa43-d8c8-4955-a683-26221a91bb05.mp4" - }, - { - "id": "2f4914b5-7511-4570-bd5a-4bc91a585655", - "created_date": "2024-07-25 07:29:44.898414", - "last_modified_date": "2024-07-25 07:29:44.898414", - "version": 0, - "url": "https://ge.xhamster.com/videos/college-fun-xh1zEsF", - "review": 0, - "should_download": 0, - "title": "College-Spa\u00df | xHamster", - "file_name": "College-Spa\u00df [xh1zEsF].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2f4914b5-7511-4570-bd5a-4bc91a585655.mp4" - }, - { - "id": "2fd0569d-a3ab-44cf-9423-0b8096c6e65e", - "created_date": "2024-07-25 07:29:47.870576", - "last_modified_date": "2024-07-25 07:29:47.870576", - "version": 0, - "url": "https://ge.xhamster.com/videos/sunde-sex-und-scharfe-katzen-1980-german-full-movie-dvd-xhHPFex", - "review": 0, - "should_download": 0, - "title": "Sunde Sex Und Scharfe Katzen 1980 German Full Movie Dvd | xHamster", - "file_name": "Sunde, Sex und scharfe Katzen (1980, German full movie, DVD) [xhHPFex].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2fd0569d-a3ab-44cf-9423-0b8096c6e65e.mp4" - }, - { - "id": "2fecc4bf-4cfd-4211-a0da-74b05427d744", - "created_date": "2024-07-25 07:29:47.700456", - "last_modified_date": "2024-07-25 07:29:47.700456", - "version": 0, - "url": "https://ge.xhamster.com/videos/super-attractive-german-chicks-getting-fucked-in-the-hot-tub-xhZUpFG", - "review": 0, - "should_download": 0, - "title": "Super attraktive deutsche K\u00fcken werden im Whirlpool gefickt | xHamster", - "file_name": "Super attraktive deutsche K\u00fcken werden im Whirlpool gefickt [xhZUpFG].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/2fecc4bf-4cfd-4211-a0da-74b05427d744.mp4" - }, - { - "id": "3052cbc6-7f0b-4b93-bd9e-188bf7e006cd", - "created_date": "2024-08-09 21:38:48.456721", - "last_modified_date": "2024-08-16 10:27:47.462000", - "version": 1, - "url": "https://ge.xhamster.com/videos/old-childhood-love-is-suddenly-at-the-door-xhBrKaD", - "review": 0, - "should_download": 0, - "title": "Alte Jugendliebe steht pl\u00f6tzlich vor der T\u00fcr! | xHamster", - "file_name": "Alte Jugendliebe steht pl\u00f6tzlich vor der T\u00fcr! [xhBrKaD].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/3052cbc6-7f0b-4b93-bd9e-188bf7e006cd.mp4" - }, - { - "id": "30c74f01-e37a-43c8-b247-171a1c167753", - "created_date": "2024-07-25 07:29:45.560139", - "last_modified_date": "2024-07-25 07:29:45.560139", - "version": 0, - "url": "https://ge.xhamster.com/videos/paolas-nude-blind-date-she-loves-being-fucked-outdoors-xh8T7ab", - "review": 0, - "should_download": 0, - "title": "Paolas nacktes Blind Date. Sie liebt es, im Freien gefickt zu werden! | xHamster", - "file_name": "Paolas nacktes Blind Date. Sie liebt es, im Freien gefickt zu werden! [xh8T7ab].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/30c74f01-e37a-43c8-b247-171a1c167753.mp4" - }, - { - "id": "30f949ed-5520-4e7d-8b05-d0981bdbf591", - "created_date": "2024-07-25 07:29:47.454322", - "last_modified_date": "2024-07-25 07:29:47.454322", - "version": 0, - "url": "https://ge.xhamster.com/videos/fick-personal-1993-xhzEPZk", - "review": 0, - "should_download": 0, - "title": "Fuck Staff (1993) | xHamster", - "file_name": "Fuck Staff (1993) [xhzEPZk].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/30f949ed-5520-4e7d-8b05-d0981bdbf591.mp4" - }, - { - "id": "3140e9e2-685b-4cc3-95f7-8605d7c90cc8", - "created_date": "2024-07-25 07:29:46.618520", - "last_modified_date": "2024-07-25 07:29:46.618520", - "version": 0, - "url": "https://ge.xhamster.com/videos/sophomore-babes-outdoors-beach-sex-orgy-6253790", - "review": 0, - "should_download": 0, - "title": "Sophomore Sch\u00e4tzchen im Freien Strand-Sex-Orgie | xHamster", - "file_name": "Sophomore Sch\u00e4tzchen im Freien Strand-Sex-Orgie [6253790].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3140e9e2-685b-4cc3-95f7-8605d7c90cc8.mp4" - }, - { - "id": "316910a1-5895-42b5-85f3-affe8cb1d7fc", - "created_date": "2024-10-21 15:08:43.557416", - "last_modified_date": "2024-10-21 16:26:43.304000", - "version": 1, - "url": "https://ge.xhamster.com/videos/outdoor-foursome-with-greta-milos-1862549", - "review": 0, - "should_download": 0, - "title": "Outdoor-Vierer mit Greta Milos | xHamster", - "file_name": "Outdoor-Vierer mit Greta Milos [1862549].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/316910a1-5895-42b5-85f3-affe8cb1d7fc.mp4" - }, - { - "id": "3189b25b-cdaf-46c6-be88-fea733df28f3", - "created_date": "2024-07-25 07:29:44.385025", - "last_modified_date": "2024-07-25 07:29:44.385025", - "version": 0, - "url": "https://ge.xhamster.com/videos/orgy-with-sperm-eating-and-snowballing-milfs-xh3iRBm", - "review": 0, - "should_download": 0, - "title": "Orgy with Sperm Eating and Snowballing MILFs: Free Porn 2c | xHamster", - "file_name": "Orgy with sperm eating and snowballing milfs [xh3iRBm].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/3189b25b-cdaf-46c6-be88-fea733df28f3.mp4" - }, - { - "id": "319852c5-8256-4fc3-829f-13d70e5e8c96", - "created_date": "2024-07-25 07:29:46.026573", - "last_modified_date": "2024-07-25 07:29:46.026573", - "version": 0, - "url": "https://ge.xhamster.com/videos/thick-ass-redhead-with-big-natural-tits-and-braces-gets-fucked-xhj06Sq", - "review": 0, - "should_download": 0, - "title": "Rothaarige mit dickem Arsch mit gro\u00dfen nat\u00fcrlichen Titten und Zahnspange wird gefickt | xHamster", - "file_name": "Rothaarige mit dickem Arsch mit gro\u00dfen nat\u00fcrlichen Titten und Zahnspange wird gefickt [xhj06Sq].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/319852c5-8256-4fc3-829f-13d70e5e8c96.mp4" - }, - { - "id": "31e206f5-9f82-4e42-84db-9958254eed26", - "created_date": "2024-07-25 07:29:45.014255", - "last_modified_date": "2024-07-25 07:29:45.014255", - "version": 0, - "url": "https://ge.xhamster.com/videos/hot-tub-orgy-at-the-neighbors-house-12975432", - "review": 0, - "should_download": 0, - "title": "Whirlpool-Orgie im Haus des Nachbarn | xHamster", - "file_name": "Whirlpool-Orgie im Haus des Nachbarn [12975432].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/31e206f5-9f82-4e42-84db-9958254eed26.mp4" - }, - { - "id": "31ffe280-bb1a-4bdb-8c4d-652ee4701099", - "created_date": "2024-07-25 07:29:47.560258", - "last_modified_date": "2024-07-25 07:29:47.560258", - "version": 0, - "url": "https://ge.xhamster.com/videos/teeny-exzesse-40-sperma-spiele-1996-xh5V9J3", - "review": 0, - "should_download": 0, - "title": "Teeny Exzesse 40 Sperma-spiele 1996, Free Porn 87 | xHamster", - "file_name": "Teeny Exzesse 40\uff1a Sperma-Spiele (1996) [xh5V9J3].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/31ffe280-bb1a-4bdb-8c4d-652ee4701099.mp4" - }, - { - "id": "323e80af-34be-44d7-a43e-9367e32ea33f", - "created_date": "2024-07-25 07:29:45.609439", - "last_modified_date": "2024-07-25 07:29:45.609439", - "version": 0, - "url": "https://ge.xhamster.com/videos/how-i-fucked-your-mother-episode-1-xhqGISD", - "review": 0, - "should_download": 0, - "title": "Wie ich deine Mutter gefickt habe - Episode 1 | xHamster", - "file_name": "Wie ich deine Mutter gefickt habe - Episode 1 [xhqGISD].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/323e80af-34be-44d7-a43e-9367e32ea33f.mp4" - }, - { - "id": "3240bb9b-0afb-4ca1-a581-ca0da71cd32e", - "created_date": "2024-07-25 07:29:44.341865", - "last_modified_date": "2024-07-25 07:29:44.341865", - "version": 0, - "url": "https://ge.xhamster.com/videos/adult-time-milf-masseuse-chanel-preston-introduces-bunny-colby-her-bf-to-3-way-nuru-massage-xhy7CTK", - "review": 0, - "should_download": 0, - "title": "ADULT TIME - MILF Masseuse Chanel Preston stellt Bunny colby und ihren freund zur 3-Wege-Nuru-Massage vor! | xHamster", - "file_name": "ADULT TIME - MILF Masseuse Chanel Preston stellt Bunny colby und ihren freund zur 3-Wege-Nuru-Massage vor! [xhy7CTK].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3240bb9b-0afb-4ca1-a581-ca0da71cd32e.mp4" - }, - { - "id": "327bf07f-8dfa-46d8-978d-5270ff125733", - "created_date": "2024-07-25 07:29:47.239338", - "last_modified_date": "2024-07-25 07:29:47.239338", - "version": 0, - "url": "https://ge.xhamster.com/videos/vanna-bardot-tells-stepbro-im-going-to-fuck-your-brains-out-tonight-s4-e9-xhyqKee", - "review": 0, - "should_download": 0, - "title": "Vanna Bardot sagt Stiefbruder: \"Ich werde heute Abend dein Gehirn ausficken\" - s4: e9 | xHamster", - "file_name": "Vanna Bardot sagt Stiefbruder\uff1a \uff02Ich werde heute Abend dein Gehirn ausficken\uff02 - s4\uff1a e9 [xhyqKee].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/327bf07f-8dfa-46d8-978d-5270ff125733.mp4" - }, - { - "id": "32914e4d-1e00-4542-9004-448cd947076c", - "created_date": "2024-08-09 21:18:37.987070", - "last_modified_date": "2024-08-16 10:29:12.596000", - "version": 1, - "url": "https://ge.xhamster.com/videos/no-shame-at-all-7239456", - "review": 0, - "should_download": 0, - "title": "Keine Schande | xHamster", - "file_name": "Keine Schande [7239456].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/32914e4d-1e00-4542-9004-448cd947076c.mp4" - }, - { - "id": "32ecb94b-8cb5-4055-af2c-5c164e187c37", - "created_date": "2024-07-25 07:29:45.525601", - "last_modified_date": "2024-07-25 07:29:45.525601", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepmom-goes-alpha-s18-e8-xhA7Ryk", - "review": 0, - "should_download": 0, - "title": "Stiefmutter goes alpha - s18: e8 | xHamster", - "file_name": "Stiefmutter goes alpha - s18\uff1a e8 [xhA7Ryk].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/32ecb94b-8cb5-4055-af2c-5c164e187c37.mp4" - }, - { - "id": "3395d2fe-c977-4b7e-8cb7-261c137d3f48", - "created_date": "2024-12-29 23:53:27.931785", - "last_modified_date": "2024-12-29 23:53:27.931785", - "version": 0, - "url": "https://ge.xhamster.com/videos/mommy-his-dick-is-stuck-in-a-vacuum-cleaner-s13-e4-xh3bfBxZ", - "review": 0, - "should_download": 0, - "title": "Mommy His Dick is Stuck in a Vacuum Cleaner - S13 E4 | xHamster", - "file_name": "Mommy His Dick Is Stuck in a Vacuum Cleaner - S13\uff1ae4 [xh3bfBxZ].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/3395d2fe-c977-4b7e-8cb7-261c137d3f48.mp4" - }, - { - "id": "3399a784-80e5-4302-b309-f73eecf9f24d", - "created_date": "2024-07-25 07:29:45.879963", - "last_modified_date": "2024-07-25 07:29:45.879963", - "version": 0, - "url": "https://ge.xhamster.com/videos/amateur-passionate-sex-on-the-boat-like-in-a-dream-xhuIura", - "review": 0, - "should_download": 0, - "title": "Amateur leidenschaftlicher SEX auf dem BOOT wie im Traum | xHamster", - "file_name": "Amateur leidenschaftlicher SEX auf dem BOOT wie im Traum [xhuIura].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3399a784-80e5-4302-b309-f73eecf9f24d.mp4" - }, - { - "id": "3415ace5-9945-41c1-8fea-17bf6c85fe0f", - "created_date": "2024-07-25 07:29:46.238729", - "last_modified_date": "2024-07-25 07:29:46.238729", - "version": 0, - "url": "https://ge.xhamster.com/videos/das-verbotene-tagebuch-der-monster-titten-9036199", - "review": 0, - "should_download": 0, - "title": "Das Verbotene Tagebuch Der Monster Titten: Free Porn a9 | xHamster", - "file_name": "Das verbotene Tagebuch der Monster Titten [9036199].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3415ace5-9945-41c1-8fea-17bf6c85fe0f.mp4" - }, - { - "id": "3455df27-241f-4e17-815b-448239d237b7", - "created_date": "2024-07-25 07:29:45.872785", - "last_modified_date": "2024-07-25 07:29:45.872785", - "version": 0, - "url": "https://ge.xhamster.com/videos/bffs-boat-party-of-teen-besties-leads-to-hardcore-pounding-with-massive-cock-xhPlWMx", - "review": 0, - "should_download": 0, - "title": "Bffs, eine Bootsparty von Teenie-Besten f\u00fchrt zu Hardcore-H\u00e4mmern mit massivem Schwanz | xHamster", - "file_name": "Bffs, eine Bootsparty von Teenie-Besten f\u00fchrt zu Hardcore-H\u00e4mmern mit massivem Schwanz [xhPlWMx].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3455df27-241f-4e17-815b-448239d237b7.mp4" - }, - { - "id": "34572338-1f9b-4b24-85bf-04676a92d7b6", - "created_date": "2024-07-25 07:29:45.094409", - "last_modified_date": "2024-07-25 07:29:45.094409", - "version": 0, - "url": "https://ge.xhamster.com/videos/it-just-slipped-in-s2-e8-xhWZ1Bd", - "review": 0, - "should_download": 0, - "title": "Es ist gerade reingelegt - s2: e8 | xHamster", - "file_name": "Es ist gerade reingelegt - s2\uff1a e8 [xhWZ1Bd].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/34572338-1f9b-4b24-85bf-04676a92d7b6.mp4" - }, - { - "id": "345d9bb2-dd9a-4e4b-86f9-3dab5af33c9c", - "created_date": "2024-07-25 07:29:46.557202", - "last_modified_date": "2024-07-25 07:29:46.557202", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepmom-to-stepdaughters-bf-i-think-i-can-help-you-with-that-problem-youve-been-having-xhgVX4o", - "review": 0, - "should_download": 0, - "title": "Stiefmutter beim freund der stieftochter \"Ich denke, ich kann dir mit diesem problem helfen, das du hattest\" | xHamster", - "file_name": "Stiefmutter beim freund der stieftochter \uff02Ich denke, ich kann dir mit diesem problem helfen, das du hattest\uff02 [xhgVX4o].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/345d9bb2-dd9a-4e4b-86f9-3dab5af33c9c.mp4" - }, - { - "id": "346fbebb-6cfc-41ad-b75a-66ae849ca54f", - "created_date": "2024-12-29 23:53:27.921639", - "last_modified_date": "2024-12-29 23:53:27.921639", - "version": 0, - "url": "https://ge.xhamster.com/videos/fucking-with-my-step-brother-s6-e2-xhV9Ifw", - "review": 0, - "should_download": 0, - "title": "Ficken mit meinem stiefbruder - S6: e2 | xHamster", - "file_name": "Ficken mit meinem stiefbruder - S6\uff1a e2 [xhV9Ifw].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/346fbebb-6cfc-41ad-b75a-66ae849ca54f.mp4" - }, - { - "id": "3499db1e-bc99-4b43-931d-62de9129d6f6", - "created_date": "2024-07-25 07:29:45.052056", - "last_modified_date": "2024-07-25 07:29:45.052056", - "version": 0, - "url": "https://ge.xhamster.com/videos/loch-um-loch-der-italienische-stecher-1984-14135545", - "review": 0, - "should_download": 0, - "title": "Loch Um Loch - Der Italienische Stecher 1984: Free Porn b0 | xHamster", - "file_name": "Loch um Loch - Der italienische Stecher (1984) [14135545].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/3499db1e-bc99-4b43-931d-62de9129d6f6.mp4" - }, - { - "id": "34c6feb5-3278-4abe-abc6-e4cd402038c4", - "created_date": "2024-07-25 07:29:48.013923", - "last_modified_date": "2024-07-25 07:29:48.013923", - "version": 0, - "url": "https://ge.xhamster.com/videos/teeny-lovers-orgasm-from-double-team-fuck-3755040", - "review": 0, - "should_download": 0, - "title": "Teenie-Liebhaber - Orgasmus vom Doppel-Team-Fick | xHamster", - "file_name": "Teenie-Liebhaber - Orgasmus vom Doppel-Team-Fick [3755040].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/34c6feb5-3278-4abe-abc6-e4cd402038c4.mp4" - }, - { - "id": "34ce1e1c-9273-40cb-98a9-bdcb3da5dd07", - "created_date": "2024-12-29 23:53:27.909583", - "last_modified_date": "2024-12-29 23:53:27.909583", - "version": 0, - "url": "https://ge.xhamster.com/videos/amateur-threesome-with-blonde-while-hubby-films-xhLEgYo", - "review": 0, - "should_download": 0, - "title": "Amateur-Dreier mit Blondine, w\u00e4hrend Ehemann filmt | xHamster", - "file_name": "Amateur-Dreier mit Blondine, w\u00e4hrend Ehemann filmt [xhLEgYo].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/34ce1e1c-9273-40cb-98a9-bdcb3da5dd07.mp4" - }, - { - "id": "35e43363-5102-4c17-a14d-867a3e34252d", - "created_date": "2024-07-25 07:29:45.605830", - "last_modified_date": "2024-07-25 07:29:45.605830", - "version": 0, - "url": "https://ge.xhamster.com/videos/two-married-couples-watch-movies-and-fuck-xh41y6s", - "review": 0, - "should_download": 0, - "title": "Zwei verheiratete Paare gucken Filme und ficken | xHamster", - "file_name": "Zwei verheiratete Paare gucken Filme und ficken [xh41y6s].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/35e43363-5102-4c17-a14d-867a3e34252d.mp4" - }, - { - "id": "363580ef-7ed9-4d84-8e1d-d105acb68e56", - "created_date": "2024-09-05 00:00:22.224000", - "last_modified_date": "2024-10-21 16:26:50.575000", - "version": 1, - "url": "https://ge.xhamster.com/videos/keep-it-in-the-family-12630000", - "review": 0, - "should_download": 0, - "title": "Behalte es in der Familie | xHamster", - "file_name": "Behalte es in der Familie [12630000].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/363580ef-7ed9-4d84-8e1d-d105acb68e56.mp4" - }, - { - "id": "3681f0e4-4825-41d1-8797-6c1e99d0f0c5", - "created_date": "2024-07-25 07:29:45.927013", - "last_modified_date": "2024-07-25 07:29:45.927013", - "version": 0, - "url": "https://ge.xhamster.com/videos/porno-villa-full-german-movie-xhh02ir", - "review": 0, - "should_download": 0, - "title": "Porno Villa - kompletter deutscher Film | xHamster", - "file_name": "Porno Villa - kompletter deutscher Film [xhh02ir].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3681f0e4-4825-41d1-8797-6c1e99d0f0c5.mp4" - }, - { - "id": "36a9b1a9-34fb-471f-9c81-7e3a6a846222", - "created_date": "2024-07-25 07:29:45.725798", - "last_modified_date": "2024-07-25 07:29:45.725798", - "version": 0, - "url": "https://ge.xhamster.com/videos/11-days-11-nights-the-house-of-pleasure-5999494", - "review": 0, - "should_download": 0, - "title": "11 Tage 11 N\u00e4chte (das Haus der Freude) | xHamster", - "file_name": "11 Tage 11 N\u00e4chte (das Haus der Freude) [5999494].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/36a9b1a9-34fb-471f-9c81-7e3a6a846222.mp4" - }, - { - "id": "36d5c16a-db94-401a-90dd-07eddd260da6", - "created_date": "2024-07-25 07:29:46.220620", - "last_modified_date": "2024-07-25 07:29:46.220620", - "version": 0, - "url": "https://ge.xhamster.com/videos/various-artist-18-and-confused-4-xhbpKdN", - "review": 0, - "should_download": 0, - "title": "Verschiedene K\u00fcnstler - 18 und verwirrt 4 | xHamster", - "file_name": "Verschiedene K\u00fcnstler - 18 und verwirrt 4 [xhbpKdN].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/36d5c16a-db94-401a-90dd-07eddd260da6.mp4" - }, - { - "id": "37017fa9-55e0-422f-bb29-50bc655059b2", - "created_date": "2025-01-16 20:33:51.011542", - "last_modified_date": "2025-01-16 20:51:32.550000", - "version": 4, - "url": "https://ge.xhamster.com/videos/never-sieep-alone-720-1984-8710064", - "review": 0, - "should_download": 0, - "title": "Nie sieb alleine 720 - 1984 | xHamster", - "file_name": "Nie sieb alleine 720 - 1984 [8710064].mp4", - "path": null, - "cloud_link": "/data/media/37017fa9-55e0-422f-bb29-50bc655059b2.mp4" - }, - { - "id": "376d59c5-007b-45a5-b197-dc911c5399b5", - "created_date": "2024-07-25 07:29:44.835625", - "last_modified_date": "2024-07-25 07:29:44.835625", - "version": 0, - "url": "https://ge.xhamster.com/videos/neighbors-wife-fucks-to-pay-for-husbands-lost-bet-xhxTXB6", - "review": 0, - "should_download": 0, - "title": "Die Frau des Nachbarn fickt, um die verlorene Wette ihres Mannes zu bezahlen | xHamster", - "file_name": "Die Frau des Nachbarn fickt, um die verlorene Wette ihres Mannes zu bezahlen [xhxTXB6].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/376d59c5-007b-45a5-b197-dc911c5399b5.mp4" - }, - { - "id": "379cc12a-c764-40cf-a7dd-1450bcc02756", - "created_date": "2024-07-25 07:29:44.977944", - "last_modified_date": "2024-07-25 07:29:44.977944", - "version": 0, - "url": "https://ge.xhamster.com/videos/blonde-german-secretary-gets-fucked-by-two-loaded-cocks-xhHt9cX", - "review": 0, - "should_download": 0, - "title": "Die blonde deutsche Sekret\u00e4rin wird von zwei geladenen Schw\u00e4nzen gefickt | xHamster", - "file_name": "Die blonde deutsche Sekret\u00e4rin wird von zwei geladenen Schw\u00e4nzen gefickt [xhHt9cX].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/379cc12a-c764-40cf-a7dd-1450bcc02756.mp4" - }, - { - "id": "37cca3ef-6db7-4221-8f46-293e6bdc1f9e", - "created_date": "2024-11-10 16:53:33.491022", - "last_modified_date": "2024-11-10 16:53:33.491022", - "version": 0, - "url": "https://ge.xhamster.com/videos/please-fuck-me-1996-8702153", - "review": 0, - "should_download": 0, - "title": "Bitte fick mich (1996) | xHamster", - "file_name": "Bitte fick mich (1996) [8702153].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/37cca3ef-6db7-4221-8f46-293e6bdc1f9e.mp4" - }, - { - "id": "37dff553-5957-45dd-ba82-6a9a199953f9", - "created_date": "2024-07-25 07:29:46.648784", - "last_modified_date": "2024-07-25 07:29:46.648784", - "version": 0, - "url": "https://ge.xhamster.com/videos/three-men-in-a-boat-to-say-nothing-of-a-pick-up-girl-scene-4800873", - "review": 0, - "should_download": 0, - "title": "Drei M\u00e4nner in einer Szene in einem Boot (ganz zu schweigen von einem Abholm\u00e4dchen) | xHamster", - "file_name": "Drei M\u00e4nner in einer Szene in einem Boot (ganz zu schweigen von einem Abholm\u00e4dchen) [4800873].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/37dff553-5957-45dd-ba82-6a9a199953f9.mp4" - }, - { - "id": "37f1e6c1-72a2-4621-b0bb-3de377e4d2e2", - "created_date": "2024-07-25 07:29:46.004817", - "last_modified_date": "2024-07-25 07:29:46.004817", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsis-says-you-want-me-to-fuck-my-stepbrother-xhp7gw3", - "review": 0, - "should_download": 0, - "title": "Stiefschwester sagt, du willst, dass ich meinen Stiefbruder ficke ?! | xHamster", - "file_name": "Stiefschwester sagt, du willst, dass ich meinen Stiefbruder ficke \uff1f! [xhp7gw3].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/37f1e6c1-72a2-4621-b0bb-3de377e4d2e2.mp4" - }, - { - "id": "37ff833a-1d49-438b-8012-44edf1ed8034", - "created_date": "2024-07-25 07:29:44.334994", - "last_modified_date": "2024-07-25 07:29:44.334994", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-new-year-s-resolution-is-to-get-creampied-xh8UWCd", - "review": 0, - "should_download": 0, - "title": "Mein Vorsatz f\u00fcrs neue Jahr ist, vollgespritzt zu werden | xHamster", - "file_name": "Mein Vorsatz f\u00fcrs neue Jahr ist, vollgespritzt zu werden [xh8UWCd].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/37ff833a-1d49-438b-8012-44edf1ed8034.mp4" - }, - { - "id": "384af2f6-2345-43e1-9bd6-0123b1f2882a", - "created_date": "2024-10-07 20:47:56.424739", - "last_modified_date": "2024-10-21 16:26:58.757000", - "version": 1, - "url": "https://ge.xhamster.com/videos/great-sex-with-a-virgin-boy-was-a-brat-became-a-man-xhso9QV", - "review": 0, - "should_download": 0, - "title": "Toller Sex mit einem jungfr\u00e4ulichen Jungen. war ein Balg, wurde ein Mann. | xHamster", - "file_name": "Toller Sex mit einem jungfr\u00e4ulichen Jungen. war ein Balg, wurde ein Mann. [xhso9QV].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/384af2f6-2345-43e1-9bd6-0123b1f2882a.mp4" - }, - { - "id": "388cee3b-976a-4c7c-b399-a8a3ddbc3598", - "created_date": "2025-01-16 19:59:58.549449", - "last_modified_date": "2025-01-16 19:59:58.549468", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-best-1-xhNv8Y4", - "review": 0, - "should_download": 0, - "title": "Das Beste 1 | xHamster", - "file_name": "Das Beste 1 [xhNv8Y4].mp4", - "path": null, - "cloud_link": "/data/media/388cee3b-976a-4c7c-b399-a8a3ddbc3598.mp4" - }, - { - "id": "389dd21e-ff53-4f6b-9442-b4cae676b3e2", - "created_date": "2024-07-25 07:29:46.249460", - "last_modified_date": "2024-07-25 07:29:46.249460", - "version": 0, - "url": "https://ge.xhamster.com/videos/tiny-hot-brunette-brooklyn-gray-opens-her-ass-cheeks-and-gets-fucked-xhk2TEc", - "review": 0, - "should_download": 0, - "title": "Die kleine hei\u00dfe Br\u00fcnette Brooklyn Grey \u00f6ffnet ihre Arschbacken und wird gefickt | xHamster", - "file_name": "Die kleine hei\u00dfe Br\u00fcnette Brooklyn Grey \u00f6ffnet ihre Arschbacken und wird gefickt [xhk2TEc].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/389dd21e-ff53-4f6b-9442-b4cae676b3e2.mp4" - }, - { - "id": "38f38ba9-708b-4d1c-a0bb-7c9f41ee8829", - "created_date": "2024-07-25 07:29:45.876368", - "last_modified_date": "2024-07-25 07:29:45.876368", - "version": 0, - "url": "https://ge.xhamster.com/videos/gang-bang-my-wife-scene-07-xhYxImd", - "review": 0, - "should_download": 0, - "title": "Gangbang mit meiner Ehefrau - Szene # 07 | xHamster", - "file_name": "Gangbang mit meiner Ehefrau - Szene # 07 [xhYxImd].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/38f38ba9-708b-4d1c-a0bb-7c9f41ee8829.mp4" - }, - { - "id": "390e7fd9-ced2-4e6e-9758-cfbd2adb0683", - "created_date": "2024-07-25 07:29:46.441817", - "last_modified_date": "2024-07-25 07:29:46.441817", - "version": 0, - "url": "https://ge.xhamster.com/videos/exxxtra-small-tiny-teen-fucked-by-her-swimming-coach-xhAZL8H", - "review": 0, - "should_download": 0, - "title": "Exxxtra small - kleines Teen von ihrem Schwimmtrainer gefickt | xHamster", - "file_name": "Exxxtra small - kleines Teen von ihrem Schwimmtrainer gefickt [xhAZL8H].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/390e7fd9-ced2-4e6e-9758-cfbd2adb0683.mp4" - }, - { - "id": "3911c5f4-c45b-4692-b141-ec71e52b57ec", - "created_date": "2025-01-16 19:59:55.961303", - "last_modified_date": "2025-01-16 19:59:55.961309", - "version": 0, - "url": "https://ge.xhamster.com/videos/babes-get-their-pussy-and-ass-fucked-in-hard-group-sex-xhFgn95", - "review": 0, - "should_download": 0, - "title": "Sch\u00e4tzchen bekommen ihre muschi und ihren arsch beim harten gruppensex gefickt | xHamster", - "file_name": "Sch\u00e4tzchen bekommen ihre muschi und ihren arsch beim harten gruppensex gefickt [xhFgn95].mp4", - "path": null, - "cloud_link": "/data/media/3911c5f4-c45b-4692-b141-ec71e52b57ec.mp4" - }, - { - "id": "3939ec81-409e-46d3-b3c4-01c161604b03", - "created_date": "2024-12-29 23:53:27.888386", - "last_modified_date": "2024-12-29 23:53:27.888386", - "version": 0, - "url": "https://ge.xhamster.com/videos/mad-max-04-chapter-01-xhtJOIu", - "review": 0, - "should_download": 0, - "title": "Mad Max # 04 - Kapitel # 01 | xHamster", - "file_name": "Mad Max # 04 - Kapitel # 01 [xhtJOIu].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/3939ec81-409e-46d3-b3c4-01c161604b03.mp4" - }, - { - "id": "393f491d-6239-4a68-b8d7-00ac984ed197", - "created_date": "2025-01-16 19:59:47.081277", - "last_modified_date": "2025-01-16 19:59:47.081284", - "version": 0, - "url": "https://ge.xhamster.com/videos/momsteachsex-horny-step-mom-tricks-teen-into-hot-threeway-7413429", - "review": 0, - "should_download": 0, - "title": "Momsteachsex - eine geile Stiefmutter trickst Teen in hei\u00dfen Dreier aus | xHamster", - "file_name": "Momsteachsex - eine geile Stiefmutter trickst Teen in hei\u00dfen Dreier aus [7413429].mp4", - "path": null, - "cloud_link": "/data/media/393f491d-6239-4a68-b8d7-00ac984ed197.mp4" - }, - { - "id": "3983b1d6-3e2a-431d-a796-4d7e2c65fffb", - "created_date": "2024-07-25 07:29:45.426324", - "last_modified_date": "2024-07-25 07:29:45.426324", - "version": 0, - "url": "https://ge.xhamster.com/videos/pure-taboo-step-parents-step-bro-welcome-new-sister-11813904", - "review": 0, - "should_download": 0, - "title": "Reines Tabu, Stiefeltern & Stiefbruder begr\u00fc\u00dfen neue Schwester | xHamster", - "file_name": "Reines Tabu, Stiefeltern & Stiefbruder begr\u00fc\u00dfen neue Schwester [11813904].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3983b1d6-3e2a-431d-a796-4d7e2c65fffb.mp4" - }, - { - "id": "399df10f-316c-466c-bac4-58dfe257b959", - "created_date": "2024-10-07 20:47:56.408036", - "last_modified_date": "2024-10-21 16:27:04.212000", - "version": 1, - "url": "https://ge.xhamster.com/videos/debuttante-35-2833809", - "review": 0, - "should_download": 0, - "title": "Debuttante 35: Free Vintage Porn Video 9f | xHamster", - "file_name": "Debuttante 35 [2833809].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/399df10f-316c-466c-bac4-58dfe257b959.mp4" - }, - { - "id": "39aaef9d-2374-4d8d-bf06-3fcfe09e3fa3", - "created_date": "2024-07-25 07:29:47.364362", - "last_modified_date": "2024-07-25 07:29:47.364362", - "version": 0, - "url": "https://ge.xhamster.com/videos/i-fucked-my-step-brother-accidentally-on-purpose-s18-e9-xhPXuax", - "review": 0, - "should_download": 0, - "title": "Ich habe meinen stiefbruer versehentlich zu einem zweck gefickt - s18: e9 | xHamster", - "file_name": "Ich habe meinen stiefbruer versehentlich zu einem zweck gefickt - s18\uff1a e9 [xhPXuax].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/39aaef9d-2374-4d8d-bf06-3fcfe09e3fa3.mp4" - }, - { - "id": "39af1a11-2632-4186-b5b9-e6ab11b9f92d", - "created_date": "2024-07-25 07:29:45.912341", - "last_modified_date": "2024-07-25 07:29:45.912341", - "version": 0, - "url": "https://ge.xhamster.com/videos/meridian-in-bed-3-guys-1552452", - "review": 0, - "should_download": 0, - "title": "Meridian im Bett & 3 Typen | xHamster", - "file_name": "Meridian im Bett & 3 Typen [1552452].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/39af1a11-2632-4186-b5b9-e6ab11b9f92d.mp4" - }, - { - "id": "39b253a0-7182-4da4-82d7-4e1759493b00", - "created_date": "2024-10-21 15:08:43.546659", - "last_modified_date": "2024-10-21 16:27:13.177000", - "version": 1, - "url": "https://ge.xhamster.com/videos/slutty-brunet-has-two-guests-over-for-dinner-then-has-kinky-threesome-1518485", - "review": 0, - "should_download": 0, - "title": "Die versaute Br\u00fcnette hat zwei G\u00e4ste zum Abendessen und hat dann einen versauten Dreier | xHamster", - "file_name": "Die versaute Br\u00fcnette hat zwei G\u00e4ste zum Abendessen und hat dann einen versauten Dreier [1518485].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/39b253a0-7182-4da4-82d7-4e1759493b00.mp4" - }, - { - "id": "39dce106-a695-44f7-96c9-5fd532066bb1", - "created_date": "2024-07-25 07:29:46.073867", - "last_modified_date": "2024-07-25 07:29:46.073867", - "version": 0, - "url": "https://ge.xhamster.com/videos/he-is-a-real-pig-xh0u0DH", - "review": 0, - "should_download": 0, - "title": "Er ist ein echtes Schwein | xHamster", - "file_name": "Er ist ein echtes Schwein [xh0u0DH].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/39dce106-a695-44f7-96c9-5fd532066bb1.mp4" - }, - { - "id": "3a3e4635-a06c-4ea4-9224-d90acdcdd463", - "created_date": "2024-07-25 07:29:47.134149", - "last_modified_date": "2024-07-25 07:29:47.134149", - "version": 0, - "url": "https://ge.xhamster.com/videos/familien-feier-endet-im-gangbang-xhhg5OR", - "review": 0, - "should_download": 0, - "title": "Familien Feier Endet Im Gangbang, Free HD Porn f5 | xHamster", - "file_name": "Familien feier endet im Gangbang [xhhg5OR].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3a3e4635-a06c-4ea4-9224-d90acdcdd463.mp4" - }, - { - "id": "3a4a9697-6575-40db-b8dc-500deb86d13e", - "created_date": "2024-07-25 07:29:45.259849", - "last_modified_date": "2024-07-25 07:29:45.259849", - "version": 0, - "url": "https://ge.xhamster.com/videos/sex-addict-secretary-fucks-a-colleague-in-front-of-her-boss-12942177", - "review": 0, - "should_download": 0, - "title": "Sexs\u00fcchtige Sekret\u00e4rin fickt eine Kollegin vor ihrem Chef | xHamster", - "file_name": "Sexs\u00fcchtige Sekret\u00e4rin fickt eine Kollegin vor ihrem Chef [12942177].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3a4a9697-6575-40db-b8dc-500deb86d13e.mp4" - }, - { - "id": "3aa4c807-b1fb-459e-9603-59b6f8cd51de", - "created_date": "2024-07-25 07:29:46.301871", - "last_modified_date": "2024-07-25 07:29:46.301871", - "version": 0, - "url": "https://ge.xhamster.com/videos/snow-white-and-7-dwarfs-1995-6305400", - "review": 0, - "should_download": 0, - "title": "Schneewittchen und 7 Zwerge (1995) | xHamster", - "file_name": "Schneewittchen und 7 Zwerge (1995) [6305400].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3aa4c807-b1fb-459e-9603-59b6f8cd51de.mp4" - }, - { - "id": "3aa9c2a0-b6ed-4d2c-ab6c-13671393fcf4", - "created_date": "2025-01-17 16:34:39.440015", - "last_modified_date": "2025-01-17 22:49:37.411000", - "version": 1, - "url": "https://ge.xhamster.com/videos/swap-your-daughter-compilation-xhsMRtd", - "review": 0, - "should_download": 0, - "title": "Tausche deine Tochter-Zusammenstellung | xHamster", - "file_name": "Tausche deine Tochter-Zusammenstellung [xhsMRtd].mp4", - "path": null, - "cloud_link": "/data/media/3aa9c2a0-b6ed-4d2c-ab6c-13671393fcf4.mp4" - }, - { - "id": "3aac90dc-962c-447d-a268-6e88f49935ce", - "created_date": "2024-07-25 07:29:46.378927", - "last_modified_date": "2024-07-25 07:29:46.378927", - "version": 0, - "url": "https://ge.xhamster.com/videos/various-artist-18-and-confused-7-xhnE4DF", - "review": 0, - "should_download": 0, - "title": "Verschiedene K\u00fcnstler - 18 und verwirrt 7 | xHamster", - "file_name": "Verschiedene K\u00fcnstler - 18 und verwirrt 7 [xhnE4DF].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3aac90dc-962c-447d-a268-6e88f49935ce.mp4" - }, - { - "id": "3abe034e-2b9b-4a5d-8a42-8b0afbe7f843", - "created_date": "2024-10-07 20:47:56.426165", - "last_modified_date": "2024-10-21 16:27:19.358000", - "version": 1, - "url": "https://ge.xhamster.com/videos/our-german-big-family-part-01-xh6mzil", - "review": 0, - "should_download": 0, - "title": "Unsere deutsche gro\u00dfe Familie !!! - Teil # 01 | xHamster", - "file_name": "Unsere deutsche gro\u00dfe Familie !!! - Teil # 01 [xh6mzil].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/3abe034e-2b9b-4a5d-8a42-8b0afbe7f843.mp4" - }, - { - "id": "3afa5947-5ecd-44e8-8823-ea5fcb2ce7aa", - "created_date": "2024-07-25 07:29:45.344148", - "last_modified_date": "2024-07-25 07:29:45.344148", - "version": 0, - "url": "https://ge.xhamster.com/videos/missax-i-did-this-for-you-charlie-forde-xhN4mIi", - "review": 0, - "should_download": 0, - "title": "Missax - ich habe das f\u00fcr dich gemacht - charlie forde | xHamster", - "file_name": "Missax - ich habe das f\u00fcr dich gemacht - charlie forde [xhN4mIi].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/3afa5947-5ecd-44e8-8823-ea5fcb2ce7aa.mp4" - }, - { - "id": "3b175348-c4dc-46b8-8e89-ac0e477f61ec", - "created_date": "2024-08-28 23:21:54.376200", - "last_modified_date": "2024-08-28 23:21:54.376200", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-roommates-ex-girlfriend-asked-me-for-a-facial-xh0b0kp", - "review": 0, - "should_download": 0, - "title": "Ex-freundin meines mitbewohners bat mich um eine gesichtsbesamung | xHamster", - "file_name": "Ex-freundin meines mitbewohners bat mich um eine gesichtsbesamung [xh0b0kp].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/3b175348-c4dc-46b8-8e89-ac0e477f61ec.mp4" - }, - { - "id": "3b256888-4e61-4a7b-b215-ef380d4ab71a", - "created_date": "2024-11-10 16:53:33.497640", - "last_modified_date": "2024-11-10 16:53:33.497640", - "version": 0, - "url": "https://ge.xhamster.com/videos/group-15-xhurxjF", - "review": 0, - "should_download": 0, - "title": "Gruppe 15 | xHamster", - "file_name": "Gruppe 15 [xhurxjF].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/3b256888-4e61-4a7b-b215-ef380d4ab71a.mp4" - }, - { - "id": "3b539f74-5d52-452f-aa83-33e88f5463a2", - "created_date": "2024-09-24 08:11:39.002314", - "last_modified_date": "2024-10-21 16:27:25.417000", - "version": 1, - "url": "https://ge.xhamster.com/videos/camping-with-a-nudist-girlfriend-compilation-xhTiwRM", - "review": 0, - "should_download": 0, - "title": "Camping mit einer fkk-freundin - zusammenstellung | xHamster", - "file_name": "Camping mit einer fkk-freundin - zusammenstellung [xhTiwRM].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/3b539f74-5d52-452f-aa83-33e88f5463a2.mp4" - }, - { - "id": "3b9a0c12-bf8c-4915-a3b9-b26830cfe5e2", - "created_date": "2024-10-21 15:08:43.545947", - "last_modified_date": "2024-10-21 16:27:29.928000", - "version": 1, - "url": "https://ge.xhamster.com/videos/caught-by-friend-with-stepbrother-blonde-teen-slut-gets-2-cocks-shoved-in-xhqmuTX", - "review": 0, - "should_download": 0, - "title": "Vom freund mit dem Stiefbruder erwischt! blonde teen schlampe bekommt 2 schwaenze rein geschoben! | xHamster", - "file_name": "Vom freund mit dem Stiefbruder erwischt! blonde teen schlampe bekommt 2 schwaenze rein geschoben! [xhqmuTX].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/3b9a0c12-bf8c-4915-a3b9-b26830cfe5e2.mp4" - }, - { - "id": "3bc516ed-cb7e-45bc-9137-57f4812131a4", - "created_date": "2024-07-25 07:29:46.892583", - "last_modified_date": "2024-07-25 07:29:46.892583", - "version": 0, - "url": "https://ge.xhamster.com/videos/la-locanda-della-maladolescenza-1980-xhkdAJh", - "review": 0, - "should_download": 0, - "title": "Das Gasthaus zur Wollust (1980) | xHamster", - "file_name": "Das Gasthaus zur Wollust (1980) [xhkdAJh].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3bc516ed-cb7e-45bc-9137-57f4812131a4.mp4" - }, - { - "id": "3c1002b0-7a5a-490c-9937-5c0b55f124e6", - "created_date": "2024-07-25 07:29:47.005079", - "last_modified_date": "2024-07-25 07:29:47.005079", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-fucking-14435642", - "review": 0, - "should_download": 0, - "title": "Familienfick | xHamster", - "file_name": "Familienfick [14435642].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3c1002b0-7a5a-490c-9937-5c0b55f124e6.mp4" - }, - { - "id": "3c438c76-6c79-407e-bf68-b78b984baf6f", - "created_date": "2024-08-09 20:20:00.727947", - "last_modified_date": "2024-08-16 10:29:46.814000", - "version": 1, - "url": "https://ge.xhamster.com/videos/strip-spin-the-bottle-with-bella-angel-kitty-kat-medison-stanley-xhoUAHX", - "review": 0, - "should_download": 0, - "title": "Strip-Spin die Flasche mit bella angel, Kitty Kat, Medison & Stanley | xHamster", - "file_name": "Strip-Spin die Flasche mit bella angel, Kitty Kat, Medison & Stanley [xhoUAHX].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/3c438c76-6c79-407e-bf68-b78b984baf6f.mp4" - }, - { - "id": "3c4de77a-f465-4b6c-ac34-f2cee5b2a76a", - "created_date": "2024-07-25 07:29:46.065119", - "last_modified_date": "2024-07-25 07:29:46.065119", - "version": 0, - "url": "https://ge.xhamster.com/videos/very-good-orgy-of-random-age-2268258", - "review": 0, - "should_download": 0, - "title": "Sehr gute Orgie zuf\u00e4lligen Alters! | xHamster", - "file_name": "Sehr gute Orgie zuf\u00e4lligen Alters! [2268258].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3c4de77a-f465-4b6c-ac34-f2cee5b2a76a.mp4" - }, - { - "id": "3c91e953-25cc-41fa-ac0b-3a7d2bb95635", - "created_date": "2024-12-29 23:53:27.881894", - "last_modified_date": "2024-12-29 23:53:27.881894", - "version": 0, - "url": "https://ge.xhamster.com/videos/clumsy-secretary-gets-fucked-like-a-slut-instead-of-being-fired-xh3os6H", - "review": 0, - "should_download": 0, - "title": "Ungeschickte Sekret\u00e4rin wird wie eine Schlampe gefickt, anstatt gefeuert zu werden! | xHamster", - "file_name": "Ungeschickte Sekret\u00e4rin wird wie eine Schlampe gefickt, anstatt gefeuert zu werden! [xh3os6H].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/3c91e953-25cc-41fa-ac0b-3a7d2bb95635.mp4" - }, - { - "id": "3cbbc7ac-ae4f-4d86-8337-1376d085f50d", - "created_date": "2024-07-25 07:29:46.997750", - "last_modified_date": "2024-07-25 07:29:46.997750", - "version": 0, - "url": "https://ge.xhamster.com/videos/shameless-boat-ride-summer-vibes-xhyKHkn", - "review": 0, - "should_download": 0, - "title": "Schamlose Schifffahrt, Sommerstimmung | xHamster", - "file_name": "Schamlose Schifffahrt, Sommerstimmung [xhyKHkn].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3cbbc7ac-ae4f-4d86-8337-1376d085f50d.mp4" - }, - { - "id": "3ce0c676-a48a-4a11-96ab-8bac42a0b284", - "created_date": "2024-07-25 07:29:46.655876", - "last_modified_date": "2024-07-25 07:29:46.655876", - "version": 0, - "url": "https://ge.xhamster.com/videos/school-excess-1996-12377529", - "review": 0, - "should_download": 0, - "title": "Schulexzess 1996 | xHamster", - "file_name": "Schulexzess 1996 [12377529].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3ce0c676-a48a-4a11-96ab-8bac42a0b284.mp4" - }, - { - "id": "3d1e7c20-3721-490e-bf60-25c14ba58fbc", - "created_date": "2024-07-25 07:29:48.172689", - "last_modified_date": "2024-07-25 07:29:48.172689", - "version": 0, - "url": "https://ge.xhamster.com/videos/husband-shares-wife-with-friend-in-threesome-1-part-xhsDq0u", - "review": 0, - "should_download": 0, - "title": "Ehemann teilt Ehefrau mit Freund zu dritt - 1 Teil | xHamster", - "file_name": "Ehemann teilt Ehefrau mit Freund zu dritt - 1 Teil [xhsDq0u].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3d1e7c20-3721-490e-bf60-25c14ba58fbc.mp4" - }, - { - "id": "3d735197-23fb-4fdb-9605-98064160e74d", - "created_date": "2024-07-25 07:29:47.202134", - "last_modified_date": "2024-07-25 07:29:47.202134", - "version": 0, - "url": "https://ge.xhamster.com/videos/cutie-fucking-a-stranger-at-the-beach-3033686", - "review": 0, - "should_download": 0, - "title": "S\u00fc\u00dfe, fickt einen Fremden am Strand | xHamster", - "file_name": "S\u00fc\u00dfe, fickt einen Fremden am Strand [3033686].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3d735197-23fb-4fdb-9605-98064160e74d.mp4" - }, - { - "id": "3de00a28-3332-443a-896e-c61e20c1e4a1", - "created_date": "2024-08-09 21:28:29.409701", - "last_modified_date": "2024-08-16 10:30:01.095000", - "version": 1, - "url": "https://ge.xhamster.com/videos/my-roommates-girlfriend-helps-me-relax-after-work-and-gets-a-huge-facial-xhwmpxJ", - "review": 0, - "should_download": 0, - "title": "Die freundin meines mitbewohners hilft mir, mich nach der arbeit zu entspannen und bekommt eine riesige gesichtsbesamung | xHamster", - "file_name": "Die freundin meines mitbewohners hilft mir, mich nach der arbeit zu entspannen und bekommt eine riesige gesichtsbesamung [xhwmpxJ].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/3de00a28-3332-443a-896e-c61e20c1e4a1.mp4" - }, - { - "id": "3e7b5f72-9f40-44d0-9322-43febb7f20d0", - "created_date": "2024-07-25 07:29:45.729331", - "last_modified_date": "2024-07-25 07:29:45.729331", - "version": 0, - "url": "https://ge.xhamster.com/videos/auf-der-couch-xhdTVv0", - "review": 0, - "should_download": 0, - "title": "Auf Der Couch: Free Porn Video e9 | xHamster", - "file_name": "Auf der Couch [xhdTVv0].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3e7b5f72-9f40-44d0-9322-43febb7f20d0.mp4" - }, - { - "id": "3ee7847b-d9ce-4f09-9287-9ef450ad4e03", - "created_date": "2024-07-25 07:29:46.465628", - "last_modified_date": "2024-07-25 07:29:46.465628", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-inc-2-full-german-movie-xhNzSgC", - "review": 0, - "should_download": 0, - "title": "Familie inkl. 2 - kompletter deutscher Film | xHamster", - "file_name": "Familie inkl. 2 - kompletter deutscher Film [xhNzSgC].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3ee7847b-d9ce-4f09-9287-9ef450ad4e03.mp4" - }, - { - "id": "3f241ea2-5ece-40c4-855a-c66e34607b83", - "created_date": "2024-11-10 16:53:33.484938", - "last_modified_date": "2024-11-10 16:53:33.484938", - "version": 0, - "url": "https://ge.xhamster.com/videos/apartment-complete-film-original-hd-version-xhXG8xg", - "review": 0, - "should_download": 0, - "title": "Wohnung (kompletter Film - Original HD-Version) | xHamster", - "file_name": "Wohnung (kompletter Film - Original HD-Version) [xhXG8xg].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/3f241ea2-5ece-40c4-855a-c66e34607b83.mp4" - }, - { - "id": "3f62e872-49b9-4f28-a481-b42a8389193a", - "created_date": "2024-07-25 07:29:45.967413", - "last_modified_date": "2024-07-25 07:29:45.967413", - "version": 0, - "url": "https://ge.xhamster.com/videos/swapsis-says-is-that-your-dick-xhqs0jZ", - "review": 0, - "should_download": 0, - "title": "Stiefschwester sagt, ist das dein Schwanz ?! | xHamster", - "file_name": "Stiefschwester sagt, ist das dein Schwanz \uff1f! [xhqs0jZ].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3f62e872-49b9-4f28-a481-b42a8389193a.mp4" - }, - { - "id": "3f63a803-fcb5-4e8d-a1a7-81d4a62c59be", - "created_date": "2024-07-25 07:29:44.938929", - "last_modified_date": "2024-07-25 07:29:44.938929", - "version": 0, - "url": "https://ge.xhamster.com/videos/barely-legal-16-2001-xh4qPHp", - "review": 0, - "should_download": 0, - "title": "Barely Legal 16 (2001) | xHamster", - "file_name": "Barely Legal 16 (2001) [xh4qPHp].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3f63a803-fcb5-4e8d-a1a7-81d4a62c59be.mp4" - }, - { - "id": "3f63cb4e-869c-418b-8bc8-e64be7f5fe56", - "created_date": "2024-07-25 07:29:45.774240", - "last_modified_date": "2024-07-25 07:29:45.774240", - "version": 0, - "url": "https://ge.xhamster.com/videos/magical-boat-ride-7217483", - "review": 0, - "should_download": 0, - "title": "Magische Bootsfahrt | xHamster", - "file_name": "Magische Bootsfahrt [7217483].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3f63cb4e-869c-418b-8bc8-e64be7f5fe56.mp4" - }, - { - "id": "3f990a9b-4574-41df-bf3d-73010d969fbc", - "created_date": "2024-07-25 07:29:47.258050", - "last_modified_date": "2024-07-25 07:29:47.258050", - "version": 0, - "url": "https://ge.xhamster.com/videos/bang-bang-yankees-party-in-usa-vol-18-xhipIwY", - "review": 0, - "should_download": 0, - "title": "Bang-Bang-Yankees-Party in den USA !!! - vol. # 18 | xHamster", - "file_name": "Bang-Bang-Yankees-Party in den USA !!! - vol. # 18 [xhipIwY].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3f990a9b-4574-41df-bf3d-73010d969fbc.mp4" - }, - { - "id": "3ffc4d9f-2ca9-4c34-bab0-f479648fcc87", - "created_date": "2024-07-25 07:29:46.126274", - "last_modified_date": "2024-07-25 07:29:46.126274", - "version": 0, - "url": "https://ge.xhamster.com/videos/nurumassage-chanel-preston-helps-young-couple-loosen-up-13426321", - "review": 0, - "should_download": 0, - "title": "Nurumassage Chanel Preston hilft jungem Paar, sich zu lockern | xHamster", - "file_name": "Nurumassage Chanel Preston hilft jungem Paar, sich zu lockern [13426321].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/3ffc4d9f-2ca9-4c34-bab0-f479648fcc87.mp4" - }, - { - "id": "40205e6b-f145-4361-ab22-5545dea1c87e", - "created_date": "2024-07-25 07:29:46.753716", - "last_modified_date": "2024-07-25 07:29:46.753716", - "version": 0, - "url": "https://ge.xhamster.com/videos/your-wife-or-mine-good-quality-7550244", - "review": 0, - "should_download": 0, - "title": "Deine Frau oder meine (gute Qualit\u00e4t) | xHamster", - "file_name": "Deine Frau oder meine (gute Qualit\u00e4t) [7550244].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/40205e6b-f145-4361-ab22-5545dea1c87e.mp4" - }, - { - "id": "409edf04-57c8-45d9-aaa1-fb32e631a51f", - "created_date": "2024-07-25 07:29:46.625934", - "last_modified_date": "2024-07-25 07:29:46.625934", - "version": 0, - "url": "https://ge.xhamster.com/videos/voyeur-and-nudist-session-with-alba-and-a-stranger-10086125", - "review": 0, - "should_download": 0, - "title": "Voyeur- und FKK-Session mit Alba und einem Fremden | xHamster", - "file_name": "Voyeur- und FKK-Session mit Alba und einem Fremden [10086125].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/409edf04-57c8-45d9-aaa1-fb32e631a51f.mp4" - }, - { - "id": "40d24e4b-fea9-4596-a400-4f92cf1b930b", - "created_date": "2024-11-10 16:53:33.471655", - "last_modified_date": "2024-11-10 16:53:33.471655", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-nephew-9030756", - "review": 0, - "should_download": 0, - "title": "Der Neffe | xHamster", - "file_name": "Der Neffe [9030756].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/40d24e4b-fea9-4596-a400-4f92cf1b930b.mp4" - }, - { - "id": "40f57318-189d-46c5-ac53-bf363fa5db24", - "created_date": "2024-07-25 07:29:45.970900", - "last_modified_date": "2024-07-25 07:29:45.970900", - "version": 0, - "url": "https://ge.xhamster.com/videos/18-and-confused-5-xhb2CTW", - "review": 0, - "should_download": 0, - "title": "18 und verwirrt # 5 | xHamster", - "file_name": "18 und verwirrt # 5 [xhb2CTW].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/40f57318-189d-46c5-ac53-bf363fa5db24.mp4" - }, - { - "id": "40fb1151-bba1-4513-b4bc-469af80de9c0", - "created_date": "2024-09-24 08:11:39.002009", - "last_modified_date": "2024-10-21 16:27:35.816000", - "version": 1, - "url": "https://ge.xhamster.com/videos/my-dirty-hobby-hard-threesome-pounding-7525930", - "review": 0, - "should_download": 0, - "title": "Mein schmutziges Hobby, harter Dreier | xHamster", - "file_name": "Mein schmutziges Hobby, harter Dreier [7525930].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/40fb1151-bba1-4513-b4bc-469af80de9c0.mp4" - }, - { - "id": "416a5b49-ba33-4b64-a988-a8029f865ce5", - "created_date": "2024-07-25 07:29:44.647148", - "last_modified_date": "2024-07-25 07:29:44.647148", - "version": 0, - "url": "https://ge.xhamster.com/videos/wife-gets-fucked-on-public-beach-by-husband-and-his-friend-ending-in-double-creampie-xhL79fR", - "review": 0, - "should_download": 0, - "title": "Ehefrau wird am \u00f6ffentlichen Strand von Ehemann und seinem Freund gefickt und endet mit doppeltem Creampie | xHamster", - "file_name": "Ehefrau wird am \u00f6ffentlichen Strand von Ehemann und seinem Freund gefickt und endet mit doppeltem Creampie [xhL79fR].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/416a5b49-ba33-4b64-a988-a8029f865ce5.mp4" - }, - { - "id": "41d6f249-c2db-47d6-a4f6-402d3cd03005", - "created_date": "2024-07-25 07:29:46.204701", - "last_modified_date": "2024-07-25 07:29:46.204701", - "version": 0, - "url": "https://ge.xhamster.com/videos/big-boobs-office-slut-fucks-big-cocked-stud-8481040", - "review": 0, - "should_download": 0, - "title": "Dicke B\u00fcro-Schlampe mit dicken M\u00f6psen fickt gro\u00dfen Schwanz | xHamster", - "file_name": "Dicke B\u00fcro-Schlampe mit dicken M\u00f6psen fickt gro\u00dfen Schwanz [8481040].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/41d6f249-c2db-47d6-a4f6-402d3cd03005.mp4" - }, - { - "id": "422f3652-2b51-4330-ba1e-b8029b8905f1", - "created_date": "2024-07-25 07:29:46.772842", - "last_modified_date": "2024-07-25 07:29:46.772842", - "version": 0, - "url": "https://ge.xhamster.com/videos/vixen-com-hot-babysitter-fucked-by-her-boss-6532667", - "review": 0, - "should_download": 0, - "title": "Vixen.com, hei\u00dfer Babysitter von ihrem Chef gefickt | xHamster", - "file_name": "Vixen.com, hei\u00dfer Babysitter von ihrem Chef gefickt [6532667].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/422f3652-2b51-4330-ba1e-b8029b8905f1.mp4" - }, - { - "id": "4234b172-df47-4cae-b389-8b3b9d4507d5", - "created_date": "2024-07-25 07:29:47.310917", - "last_modified_date": "2024-07-25 07:29:47.310917", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-fun-fantasy-xhJlIqZ", - "review": 0, - "should_download": 0, - "title": "Familienspa\u00df-Fantasie | xHamster", - "file_name": "Familienspa\u00df-Fantasie [xhJlIqZ].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4234b172-df47-4cae-b389-8b3b9d4507d5.mp4" - }, - { - "id": "4235a122-d2ee-405e-a8ae-3194b841311e", - "created_date": "2024-07-25 07:29:44.965154", - "last_modified_date": "2024-07-25 07:29:44.965154", - "version": 0, - "url": "https://ge.xhamster.com/videos/step-sister-likes-to-be-naked-xhS7JBS", - "review": 0, - "should_download": 0, - "title": "Stiefschwester ist gerne nackt | xHamster", - "file_name": "Stiefschwester ist gerne nackt [xhS7JBS].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4235a122-d2ee-405e-a8ae-3194b841311e.mp4" - }, - { - "id": "424328cf-9035-4df1-8cdc-42115d8d51a3", - "created_date": "2024-10-21 15:08:43.549133", - "last_modified_date": "2024-10-21 16:27:46.678000", - "version": 1, - "url": "https://ge.xhamster.com/videos/the-swinging-seventies-threesome-mfm-scene-4445648", - "review": 0, - "should_download": 0, - "title": "The Swinging Seventies (Dreier-MFM-Szene) | xHamster", - "file_name": "The Swinging Seventies (Dreier-MFM-Szene) [4445648].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/424328cf-9035-4df1-8cdc-42115d8d51a3.mp4" - }, - { - "id": "42804d74-c912-4d28-8122-51d331ae5ca1", - "created_date": "2024-07-25 07:29:44.465755", - "last_modified_date": "2024-07-25 07:29:44.465755", - "version": 0, - "url": "https://ge.xhamster.com/videos/have-fun-with-our-neighbors-7742107", - "review": 0, - "should_download": 0, - "title": "Haben Sie Spa\u00df mit unseren Nachbarn | xHamster", - "file_name": "Haben Sie Spa\u00df mit unseren Nachbarn [7742107].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/42804d74-c912-4d28-8122-51d331ae5ca1.mp4" - }, - { - "id": "431312ad-324a-430f-bbd8-26c5daba4ce9", - "created_date": "2024-07-25 07:29:45.951959", - "last_modified_date": "2024-07-25 07:29:45.951959", - "version": 0, - "url": "https://ge.xhamster.com/videos/familie-immerscharf-4-10812927", - "review": 0, - "should_download": 0, - "title": "Familie Immerscharf 4, Free HD Porn Video 66 | xHamster", - "file_name": "Familie Immerscharf 4 [10812927].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/431312ad-324a-430f-bbd8-26c5daba4ce9.mp4" - }, - { - "id": "4330c224-a07f-469c-96e0-cb9dc2d5df34", - "created_date": "2024-07-25 07:29:45.008992", - "last_modified_date": "2024-07-25 07:29:45.008992", - "version": 0, - "url": "https://ge.xhamster.com/videos/a-couple-receives-friends-for-an-orgy-xhwvb3h", - "review": 0, - "should_download": 0, - "title": "Ein Paar empf\u00e4ngt Freunde f\u00fcr eine Orgie | xHamster", - "file_name": "Ein Paar empf\u00e4ngt Freunde f\u00fcr eine Orgie [xhwvb3h].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/4330c224-a07f-469c-96e0-cb9dc2d5df34.mp4" - }, - { - "id": "4342200b-e168-41e6-b536-28c1ff407a9e", - "created_date": "2024-07-25 07:29:44.472957", - "last_modified_date": "2024-07-25 07:29:44.472957", - "version": 0, - "url": "https://ge.xhamster.com/videos/aunt-and-her-cougar-friend-find-nephew-upscaled-to-4k-xhnjYpQ", - "review": 0, - "should_download": 0, - "title": "Tante und ihre MILF-Freundin finden Neffen, auf 4k hochskaliert | xHamster", - "file_name": "Tante und ihre MILF-Freundin finden Neffen, auf 4k hochskaliert [xhnjYpQ].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4342200b-e168-41e6-b536-28c1ff407a9e.mp4" - }, - { - "id": "43464a08-9992-46f0-b6bc-d824c7f0f006", - "created_date": "2024-07-25 07:29:44.639836", - "last_modified_date": "2024-07-25 07:29:44.639836", - "version": 0, - "url": "https://ge.xhamster.com/videos/fucked-on-mallorca-vacation-by-2-guys-ao-10090812", - "review": 0, - "should_download": 0, - "title": "Im Mallorca Urlaub von 2 Kerlen AO Abgefickt | xHamster", - "file_name": "Im Mallorca Urlaub von 2 Kerlen AO Abgefickt [10090812].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/43464a08-9992-46f0-b6bc-d824c7f0f006.mp4" - }, - { - "id": "43669ed5-bb65-432a-8b43-389a085fb4da", - "created_date": "2024-12-29 23:53:27.910528", - "last_modified_date": "2024-12-29 23:53:27.910528", - "version": 0, - "url": "https://ge.xhamster.com/videos/dbm-sex-society-xhRYvMF", - "review": 0, - "should_download": 0, - "title": "Dbm - Sexgesellschaft | xHamster", - "file_name": "Dbm - Sexgesellschaft [xhRYvMF].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/43669ed5-bb65-432a-8b43-389a085fb4da.mp4" - }, - { - "id": "43ea40a4-cd60-44a2-b5ef-de786c6c4a4e", - "created_date": "2024-10-21 15:08:43.547605", - "last_modified_date": "2024-10-21 16:27:52.649000", - "version": 1, - "url": "https://ge.xhamster.com/videos/gangbang-at-the-cocktail-bar-5737462", - "review": 0, - "should_download": 0, - "title": "Gangbang an der Cocktailbar | xHamster", - "file_name": "Gangbang an der Cocktailbar [5737462].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/43ea40a4-cd60-44a2-b5ef-de786c6c4a4e.mp4" - }, - { - "id": "44031dbc-8226-4a0e-add4-3e362f069e74", - "created_date": "2024-07-25 07:29:45.624898", - "last_modified_date": "2024-07-25 07:29:45.624898", - "version": 0, - "url": "https://ge.xhamster.com/videos/just-vintage-339-xhYtEPo", - "review": 0, - "should_download": 0, - "title": "Nur Retro 339 | xHamster", - "file_name": "Nur Retro 339 [xhYtEPo].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/44031dbc-8226-4a0e-add4-3e362f069e74.mp4" - }, - { - "id": "442b1d23-72d3-4b4a-9a91-0f05d538cc02", - "created_date": "2024-07-25 07:29:47.759426", - "last_modified_date": "2024-07-25 07:29:47.759426", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepmom-agrees-to-let-stepson-and-his-bff-dp-her-xhM7ESl", - "review": 0, - "should_download": 0, - "title": "Stiefmutter ist damit einverstanden, Stiefsohn und seine Freundin sie doppelpenetrieren zu lassen | xHamster", - "file_name": "Stiefmutter ist damit einverstanden, Stiefsohn und seine Freundin sie doppelpenetrieren zu lassen [xhM7ESl].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/442b1d23-72d3-4b4a-9a91-0f05d538cc02.mp4" - }, - { - "id": "44652828-e6dc-427f-a510-a873ebc602a5", - "created_date": "2024-07-25 07:29:45.367673", - "last_modified_date": "2024-07-25 07:29:45.367673", - "version": 0, - "url": "https://ge.xhamster.com/videos/naughty-schoolgirls-13473759", - "review": 0, - "should_download": 0, - "title": "Schulm\u00e4dchen im Reifetest | xHamster", - "file_name": "Schulm\u00e4dchen im Reifetest [13473759].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/44652828-e6dc-427f-a510-a873ebc602a5.mp4" - }, - { - "id": "44d408d0-803d-4c20-b2d0-2a649b8ce4f6", - "created_date": "2024-07-25 07:29:46.987038", - "last_modified_date": "2024-07-25 07:29:46.987038", - "version": 0, - "url": "https://ge.xhamster.com/videos/happy-wife-happy-life-s41-e15-xheo9jG", - "review": 0, - "should_download": 0, - "title": "Gl\u00fcckliche Ehefrau, gl\u00fcckliches Leben - s41: e15 | xHamster", - "file_name": "Gl\u00fcckliche Ehefrau, gl\u00fcckliches Leben - s41\uff1a e15 [xheo9jG].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/44d408d0-803d-4c20-b2d0-2a649b8ce4f6.mp4" - }, - { - "id": "44e900f4-d248-4959-a5fd-e47379309f8b", - "created_date": "2024-07-25 07:29:45.157943", - "last_modified_date": "2024-07-25 07:29:45.157943", - "version": 0, - "url": "https://ge.xhamster.com/videos/youre-not-listening-to-me-again-you-mean-girl-ill-make-you-study-xhqYaQK", - "review": 0, - "should_download": 0, - "title": "Du h\u00f6rst mir nicht wieder zu, du meinst m\u00e4dchen, ich werde dich zum lernen bringen! | xHamster", - "file_name": "Du h\u00f6rst mir nicht wieder zu, du meinst m\u00e4dchen, ich werde dich zum lernen bringen! [xhqYaQK].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/44e900f4-d248-4959-a5fd-e47379309f8b.mp4" - }, - { - "id": "4504a4bd-2e80-4500-a499-1536883a3f6c", - "created_date": "2024-07-25 07:29:45.804055", - "last_modified_date": "2024-07-25 07:29:45.804055", - "version": 0, - "url": "https://ge.xhamster.com/videos/robin-s-nest-1980-xhc3zbM", - "review": 0, - "should_download": 0, - "title": "Robin's Nest (1980) | xHamster", - "file_name": "Robin's Nest (1980) [xhc3zbM].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4504a4bd-2e80-4500-a499-1536883a3f6c.mp4" - }, - { - "id": "450c8826-6ac8-482f-9481-992af2fa8da5", - "created_date": "2024-07-25 07:29:47.874081", - "last_modified_date": "2024-07-25 07:29:47.874081", - "version": 0, - "url": "https://ge.xhamster.com/videos/getting-fucked-brings-her-joy-xhTYtEB", - "review": 0, - "should_download": 0, - "title": "Gefickt zu werden macht ihr Freude | xHamster", - "file_name": "Gefickt zu werden macht ihr Freude [xhTYtEB].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/450c8826-6ac8-482f-9481-992af2fa8da5.mp4" - }, - { - "id": "451b3813-4101-4611-9aa5-c01dc767c2f3", - "created_date": "2024-08-28 23:21:54.365379", - "last_modified_date": "2024-08-28 23:21:54.365379", - "version": 0, - "url": "https://ge.xhamster.com/videos/party-games-turn-into-party-sex-with-sakura-and-friends-xhkMtW1", - "review": 0, - "should_download": 0, - "title": "Partyspiele werden zu Partysex mit Sakura und Freunden | xHamster", - "file_name": "Partyspiele werden zu Partysex mit Sakura und Freunden [xhkMtW1].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/451b3813-4101-4611-9aa5-c01dc767c2f3.mp4" - }, - { - "id": "456953b6-a24d-4dc0-af79-bf710f88d7ac", - "created_date": "2024-07-25 07:29:46.068974", - "last_modified_date": "2024-07-25 07:29:46.068974", - "version": 0, - "url": "https://ge.xhamster.com/videos/group-sex-on-vacation-on-mallorca-xhgWLoV", - "review": 0, - "should_download": 0, - "title": "Geiler Gruppensex im Urlaub auf Mallorca | xHamster", - "file_name": "Geiler Gruppensex im Urlaub auf Mallorca [xhgWLoV].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/456953b6-a24d-4dc0-af79-bf710f88d7ac.mp4" - }, - { - "id": "45758a88-4bfb-4e42-a6e2-cb5b274b695e", - "created_date": "2024-07-25 07:29:45.166320", - "last_modified_date": "2024-07-25 07:29:45.166320", - "version": 0, - "url": "https://ge.xhamster.com/videos/college-teen-pussypounded-in-university-dorm-7219848", - "review": 0, - "should_download": 0, - "title": "College-Teen pussypounded im Studentenwohnheim | xHamster", - "file_name": "College-Teen pussypounded im Studentenwohnheim [7219848].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/45758a88-4bfb-4e42-a6e2-cb5b274b695e.mp4" - }, - { - "id": "460ab2c7-9b06-43fe-91e4-2b36ee9bc1dd", - "created_date": "2024-07-25 07:29:48.142999", - "last_modified_date": "2024-07-25 07:29:48.142999", - "version": 0, - "url": "https://ge.xhamster.com/videos/thirsty-work-1992-full-movie-xhWlLcH", - "review": 0, - "should_download": 0, - "title": "Durstige arbeit (1992) kompletter film | xHamster", - "file_name": "Durstige arbeit (1992) kompletter film [xhWlLcH].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/460ab2c7-9b06-43fe-91e4-2b36ee9bc1dd.mp4" - }, - { - "id": "4625b0f7-8f84-4ccb-b4b7-47ab8efefeeb", - "created_date": "2024-07-25 07:29:47.779586", - "last_modified_date": "2024-07-25 07:29:47.779586", - "version": 0, - "url": "https://ge.xhamster.com/videos/stossgebet-1979-10792310", - "review": 0, - "should_download": 0, - "title": "Stossgebet 1979: Free Retro Porn Video ac | xHamster", - "file_name": "Stossgebet (1979) [10792310].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4625b0f7-8f84-4ccb-b4b7-47ab8efefeeb.mp4" - }, - { - "id": "4679da1e-7a53-4a94-853f-ffd154aad647", - "created_date": "2024-07-25 07:29:44.318487", - "last_modified_date": "2024-07-25 07:29:44.318487", - "version": 0, - "url": "https://ge.xhamster.com/videos/taboo-fun-3806995", - "review": 0, - "should_download": 0, - "title": "Taboo Fun: Free Mature & MILF Porn Video 64 | xHamster", - "file_name": "taboo fun [3806995].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4679da1e-7a53-4a94-853f-ffd154aad647.mp4" - }, - { - "id": "46adba2b-5357-45db-a9a6-41d89f4fdc1a", - "created_date": "2024-07-25 07:29:47.418766", - "last_modified_date": "2024-07-25 07:29:47.418766", - "version": 0, - "url": "https://ge.xhamster.com/videos/german-family-secrets-episode-04-xh5vD3q", - "review": 0, - "should_download": 0, - "title": "Deutsche Familiengeheimnisse !!! - (Episode # 04) | xHamster", - "file_name": "Deutsche Familiengeheimnisse !!! - (Episode # 04) [xh5vD3q].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/46adba2b-5357-45db-a9a6-41d89f4fdc1a.mp4" - }, - { - "id": "46f2fbc6-d8b9-4aed-9024-428f066b509a", - "created_date": "2024-07-25 07:29:46.675345", - "last_modified_date": "2024-07-25 07:29:46.675345", - "version": 0, - "url": "https://ge.xhamster.com/videos/meet-the-nudists-7678194", - "review": 0, - "should_download": 0, - "title": "Treffen Sie die Nudisten | xHamster", - "file_name": "Treffen Sie die Nudisten [7678194].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/46f2fbc6-d8b9-4aed-9024-428f066b509a.mp4" - }, - { - "id": "46f79d78-587a-4b11-b536-47da2584126c", - "created_date": "2024-07-25 07:29:47.885135", - "last_modified_date": "2024-07-25 07:29:47.885135", - "version": 0, - "url": "https://ge.xhamster.com/videos/i-have-a-very-hands-on-approach-to-anatomy-xhpNmXX", - "review": 0, - "should_download": 0, - "title": "Ich habe einen sehr praktischen Zugang zur Anatomie | xHamster", - "file_name": "Ich habe einen sehr praktischen Zugang zur Anatomie [xhpNmXX].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/46f79d78-587a-4b11-b536-47da2584126c.mp4" - }, - { - "id": "471292ac-b5e2-4077-940d-3c18a14699c6", - "created_date": "2024-07-25 07:29:45.752821", - "last_modified_date": "2024-07-25 07:29:45.752821", - "version": 0, - "url": "https://ge.xhamster.com/videos/i-share-a-bed-with-my-stepmom-and-her-friend-then-we-fuck-xhURTsf", - "review": 0, - "should_download": 0, - "title": "ICH TEILE EIN BETT MIT MEINER STIEFMUTTER UND IHRER FREUNDIN, DANN FICKEN WIR. | xHamster", - "file_name": "ICH TEILE EIN BETT MIT MEINER STIEFMUTTER UND IHRER FREUNDIN, DANN FICKEN WIR. [xhURTsf].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/471292ac-b5e2-4077-940d-3c18a14699c6.mp4" - }, - { - "id": "474e2e7f-0654-448e-9144-c749371f96b5", - "created_date": "2024-07-25 07:29:47.088713", - "last_modified_date": "2024-07-25 07:29:47.088713", - "version": 0, - "url": "https://ge.xhamster.com/videos/bea-dumas-wedding-reception-orgy-this-is-hot-998036", - "review": 0, - "should_download": 0, - "title": "Bea Dumas Hochzeitsempfang-Orgie! Das ist hei\u00df !! | xHamster", - "file_name": "Bea Dumas Hochzeitsempfang-Orgie! Das ist hei\u00df !! [998036].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/474e2e7f-0654-448e-9144-c749371f96b5.mp4" - }, - { - "id": "47513996-9e2b-4a6c-ab1e-13dc3e6411eb", - "created_date": "2024-07-25 07:29:47.825054", - "last_modified_date": "2024-07-25 07:29:47.825054", - "version": 0, - "url": "https://ge.xhamster.com/videos/full-house-college-party-turns-into-hardcore-orgy-1583468", - "review": 0, - "should_download": 0, - "title": "Full House College-Party wird zu Hardcore-Orgie | xHamster", - "file_name": "Full House College-Party wird zu Hardcore-Orgie [1583468].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/47513996-9e2b-4a6c-ab1e-13dc3e6411eb.mp4" - }, - { - "id": "4752be84-66b9-4540-979e-d33aba799b08", - "created_date": "2024-07-25 07:29:47.585924", - "last_modified_date": "2024-07-25 07:29:47.585924", - "version": 0, - "url": "https://ge.xhamster.com/videos/kelly-trump-classic-hotel-of-pleasure-xhA7OFC", - "review": 0, - "should_download": 0, - "title": "Hotel der L\u00fcste | xHamster", - "file_name": "Hotel der L\u00fcste [xhA7OFC].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4752be84-66b9-4540-979e-d33aba799b08.mp4" - }, - { - "id": "477e96c7-07db-4103-a1df-39c2ba87b963", - "created_date": "2024-07-25 07:29:47.672621", - "last_modified_date": "2024-07-25 07:29:47.672621", - "version": 0, - "url": "https://ge.xhamster.com/videos/rural-holidays-1999-russian-full-video-hdtv-rip-xh61wb4", - "review": 0, - "should_download": 0, - "title": "L\u00e4ndliche Feiertage (1999, russisch, volles Video, hdtv rip) | xHamster", - "file_name": "L\u00e4ndliche Feiertage (1999, russisch, volles Video, hdtv rip) [xh61wb4].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/477e96c7-07db-4103-a1df-39c2ba87b963.mp4" - }, - { - "id": "47dc2c2b-590f-480e-9004-7ffed620845a", - "created_date": "2024-07-25 07:29:45.756413", - "last_modified_date": "2024-07-25 07:29:45.756413", - "version": 0, - "url": "https://ge.xhamster.com/videos/but-wait-you-were-only-supposed-to-jerk-him-off-lovita-fate-yells-at-ivi-rein-s3-e5-xhaBC2o", - "review": 0, - "should_download": 0, - "title": "\"Aber warten Sie! Sie sollten ihn nur wichsen!\" Lovita Fate schreit ivi rein -s3: e5 an | xHamster", - "file_name": "\uff02Aber warten Sie! Sie sollten ihn nur wichsen!\uff02 Lovita Fate schreit ivi rein -s3\uff1a e5 an [xhaBC2o].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/47dc2c2b-590f-480e-9004-7ffed620845a.mp4" - }, - { - "id": "47f81a5c-6c9b-4452-b9cd-6b64b2cdce8a", - "created_date": "2024-07-25 07:29:47.669210", - "last_modified_date": "2024-07-25 07:29:47.669210", - "version": 0, - "url": "https://ge.xhamster.com/videos/inside-desiree-cousteau-1979-xh47iIM", - "review": 0, - "should_download": 0, - "title": "Inside Desiree Cousteau (1979) | xHamster", - "file_name": "Inside Desiree Cousteau (1979) [xh47iIM].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/47f81a5c-6c9b-4452-b9cd-6b64b2cdce8a.mp4" - }, - { - "id": "48491c32-2de7-4e73-ab4c-d3ae50f67aa3", - "created_date": "2024-07-25 07:29:45.286540", - "last_modified_date": "2024-07-25 07:29:45.286540", - "version": 0, - "url": "https://ge.xhamster.com/videos/new-schoolgirl-lizz-gets-initiated-by-faye-and-her-roommates-14606088", - "review": 0, - "should_download": 0, - "title": "Neues Schulm\u00e4dchen Lizz wird von Faye und ihren Mitbewohnern initiiert | xHamster", - "file_name": "Neues Schulm\u00e4dchen Lizz wird von Faye und ihren Mitbewohnern initiiert [14606088].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/48491c32-2de7-4e73-ab4c-d3ae50f67aa3.mp4" - }, - { - "id": "48898536-8e2a-4234-9b12-22701dc622f3", - "created_date": "2024-07-25 07:29:47.403170", - "last_modified_date": "2024-07-25 07:29:47.403170", - "version": 0, - "url": "https://ge.xhamster.com/videos/classic-danish-gloryhole-xhvprSD", - "review": 0, - "should_download": 0, - "title": "Klassischer D\u00e4nisch - Gloryhole | xHamster", - "file_name": "Klassischer D\u00e4nisch - Gloryhole [xhvprSD].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/48898536-8e2a-4234-9b12-22701dc622f3.mp4" - }, - { - "id": "48932c6f-51f1-4c82-92ea-9127b78e6d86", - "created_date": "2024-09-11 10:23:29.175727", - "last_modified_date": "2024-10-21 16:27:59.603000", - "version": 1, - "url": "https://ge.xhamster.com/videos/step-mom-lets-step-son-free-use-his-step-sis-amber-moore-all-around-the-house-freeuse-fantasy-xhOvYEF", - "review": 0, - "should_download": 0, - "title": "Stiefmutter l\u00e4sst stiefsohn seine stiefschwester amber moore rund um das haus benutzen - freie fantasie | xHamster", - "file_name": "Stiefmutter l\u00e4sst stiefsohn seine stiefschwester amber moore rund um das haus benutzen - freie fantasie [xhOvYEF].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/48932c6f-51f1-4c82-92ea-9127b78e6d86.mp4" - }, - { - "id": "489be12e-90a6-4238-90e8-1120a77666aa", - "created_date": "2024-10-14 20:33:38.257194", - "last_modified_date": "2024-10-21 16:28:03.856000", - "version": 1, - "url": "https://ge.xhamster.com/videos/party-queen-3298277", - "review": 0, - "should_download": 0, - "title": "Partyk\u00f6nigin | xHamster", - "file_name": "Partyk\u00f6nigin [3298277].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/489be12e-90a6-4238-90e8-1120a77666aa.mp4" - }, - { - "id": "48c2c5de-6e77-4162-aeb2-65f905fda752", - "created_date": "2024-07-25 07:29:44.968836", - "last_modified_date": "2024-07-25 07:29:44.968836", - "version": 0, - "url": "https://ge.xhamster.com/videos/landschlampen-vol-2-full-movie-xh24aFo", - "review": 0, - "should_download": 0, - "title": "Landschlampen vol.2 (kompletter Film) | xHamster", - "file_name": "Landschlampen vol.2 (kompletter Film) [xh24aFo].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/48c2c5de-6e77-4162-aeb2-65f905fda752.mp4" - }, - { - "id": "48cadf46-73d1-444f-a93e-9ef0753d5611", - "created_date": "2024-07-25 07:29:46.034301", - "last_modified_date": "2024-07-25 07:29:46.034301", - "version": 0, - "url": "https://ge.xhamster.com/videos/now-this-is-how-to-party-723551", - "review": 0, - "should_download": 0, - "title": "Nun, so wird gefeiert | xHamster", - "file_name": "Nun, so wird gefeiert [723551].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/48cadf46-73d1-444f-a93e-9ef0753d5611.mp4" - }, - { - "id": "48d80238-6845-482c-a7ce-523c88558eff", - "created_date": "2024-07-25 07:29:44.281339", - "last_modified_date": "2024-07-25 07:29:44.281339", - "version": 0, - "url": "https://ge.xhamster.com/videos/what-movie-is-this-12278374", - "review": 0, - "should_download": 0, - "title": "Was f\u00fcr ein Film ist das? | xHamster", - "file_name": "Was f\u00fcr ein Film ist das\uff1f [12278374].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/48d80238-6845-482c-a7ce-523c88558eff.mp4" - }, - { - "id": "4932260e-32da-462d-8d33-591ecc24e3ff", - "created_date": "2024-07-25 07:29:45.404426", - "last_modified_date": "2024-07-25 07:29:45.404426", - "version": 0, - "url": "https://ge.xhamster.com/videos/missax-watching-porn-with-charlie-forde-xhSMBph", - "review": 0, - "should_download": 0, - "title": "MissaX - Porno gucken mit Charlie Forde | xHamster", - "file_name": "MissaX - Porno gucken mit Charlie Forde [xhSMBph].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/4932260e-32da-462d-8d33-591ecc24e3ff.mp4" - }, - { - "id": "49355c65-c1f9-49d3-aedd-4415629449c7", - "created_date": "2024-07-25 07:29:44.331429", - "last_modified_date": "2024-07-25 07:29:44.331429", - "version": 0, - "url": "https://ge.xhamster.com/videos/studentin-macht-gerne-die-beine-breit-xhaCkkP", - "review": 0, - "should_download": 0, - "title": "Studentin Macht Gerne Die Beine Breit, Porn a4 | xHamster", - "file_name": "Studentin macht gerne die Beine breit [xhaCkkP].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/49355c65-c1f9-49d3-aedd-4415629449c7.mp4" - }, - { - "id": "4944540e-3960-4905-9c6f-5af05e9fcd6f", - "created_date": "2024-08-08 01:01:27.749190", - "last_modified_date": "2024-08-08 01:01:27.749190", - "version": 0, - "url": null, - "review": 0, - "should_download": 0, - "title": null, - "file_name": "Familiensex [442392431].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4944540e-3960-4905-9c6f-5af05e9fcd6f.mp4" - }, - { - "id": "49812d30-3d9b-4f89-b80d-eec50c1e454e", - "created_date": "2024-07-25 07:29:45.662046", - "last_modified_date": "2024-07-25 07:29:45.662046", - "version": 0, - "url": "https://ge.xhamster.com/videos/step-son-and-his-best-friend-seduce-petite-step-mom-katrina-colt-to-fuck-in-a-threesome-mylf-taboo-xhIDUtM", - "review": 0, - "should_download": 0, - "title": "Stiefsohn und sein bester freund verf\u00fchren zierliche stiefmutter katrina colt zum dreier - MYLF tabu | xHamster", - "file_name": "Stiefsohn und sein bester freund verf\u00fchren zierliche stiefmutter katrina colt zum dreier - MYLF tabu [xhIDUtM].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/49812d30-3d9b-4f89-b80d-eec50c1e454e.mp4" - }, - { - "id": "498987ba-78e8-47a5-a5e0-27deaf5ab554", - "created_date": "2024-12-29 23:53:27.959278", - "last_modified_date": "2024-12-29 23:53:27.959278", - "version": 0, - "url": "https://ge.xhamster.com/videos/1976-jennifer-welles-of-a-young-american-house-wife-7516415", - "review": 0, - "should_download": 0, - "title": "1976 Jennifer Welles einer jungen amerikanischen Hausfrau | xHamster", - "file_name": "1976 Jennifer Welles einer jungen amerikanischen Hausfrau [7516415].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/498987ba-78e8-47a5-a5e0-27deaf5ab554.mp4" - }, - { - "id": "49fc1216-dd76-4c2d-8d9b-37a76d8ae976", - "created_date": "2024-07-25 07:29:45.293699", - "last_modified_date": "2024-07-25 07:29:45.293699", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-joys-of-our-neighborhood-3675552", - "review": 0, - "should_download": 0, - "title": "Die Freuden unserer Nachbarschaft | xHamster", - "file_name": "Die Freuden unserer Nachbarschaft [3675552].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/49fc1216-dd76-4c2d-8d9b-37a76d8ae976.mp4" - }, - { - "id": "4a3f670d-d8c6-47a8-a45d-3010a8f60872", - "created_date": "2024-07-25 07:29:46.510928", - "last_modified_date": "2024-07-25 07:29:46.510928", - "version": 0, - "url": "https://ge.xhamster.com/videos/one-of-the-dirtiest-families-ever-01-272364", - "review": 0, - "should_download": 0, - "title": "Eine der schmutzigsten Familien aller Zeiten | xHamster", - "file_name": "Eine der schmutzigsten Familien aller Zeiten [272364].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4a3f670d-d8c6-47a8-a45d-3010a8f60872.mp4" - }, - { - "id": "4a836b1e-b092-4efe-8d8f-03b250581b05", - "created_date": "2024-07-25 07:29:46.575999", - "last_modified_date": "2024-07-25 07:29:46.575999", - "version": 0, - "url": "https://ge.xhamster.com/videos/brother-step-sister-share-a-girlfriend-family-therapy-xhmMqlw", - "review": 0, - "should_download": 0, - "title": "Bruder und Stiefschwester teilen sich eine Freundin - Familientherapie | xHamster", - "file_name": "Bruder und Stiefschwester teilen sich eine Freundin - Familientherapie [xhmMqlw].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4a836b1e-b092-4efe-8d8f-03b250581b05.mp4" - }, - { - "id": "4aa684dc-07d4-4cae-893d-94683f47414d", - "created_date": "2025-01-19 13:42:34.178363", - "last_modified_date": "2025-01-19 13:42:34.178369", - "version": 0, - "url": "https://ge.xhamster.com/videos/passionate-mormon-teens-enjoy-pleasing-the-priests-xhyhWNM", - "review": 0, - "should_download": 0, - "title": "Leidenschaftliche mormonische teenager genie\u00dfen es, die priester zu befriedigen | xHamster", - "file_name": "Leidenschaftliche mormonische teenager genie\u00dfen es, die priester zu befriedigen [xhyhWNM].mp4", - "path": null, - "cloud_link": null - }, - { - "id": "4abd106c-1a12-44e8-97ef-c666b11dcff6", - "created_date": "2024-07-25 07:29:45.025535", - "last_modified_date": "2024-07-25 07:29:45.025535", - "version": 0, - "url": "https://ge.xhamster.com/videos/perfect-redhead-daughter-fucks-step-step-dad-opal-essex-xhQlllr", - "review": 0, - "should_download": 0, - "title": "Perfekte rothaarige Tochter fickt Stiefvater - Opal Essex | xHamster", - "file_name": "Perfekte rothaarige Tochter fickt Stiefvater - Opal Essex [xhQlllr].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4abd106c-1a12-44e8-97ef-c666b11dcff6.mp4" - }, - { - "id": "4b4544f4-f933-4945-82e7-1d8f3b1176e3", - "created_date": "2024-07-25 07:29:44.893563", - "last_modified_date": "2024-07-25 07:29:44.893563", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-best-group-fuck-in-town-1-xhYEPOj", - "review": 0, - "should_download": 0, - "title": "Der beste gruppenfick in der stadt 1 | xHamster", - "file_name": "Der beste gruppenfick in der stadt 1 [xhYEPOj].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4b4544f4-f933-4945-82e7-1d8f3b1176e3.mp4" - }, - { - "id": "4b5dfe03-1f9b-4fac-8576-0cc8d73841fb", - "created_date": "2024-07-25 07:29:44.934868", - "last_modified_date": "2024-07-25 07:29:44.934868", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-secrets-full-movie-xhvZIzI", - "review": 0, - "should_download": 0, - "title": "Familiengeheimnisse - kompletter Film | xHamster", - "file_name": "Familiengeheimnisse - kompletter Film [xhvZIzI].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/4b5dfe03-1f9b-4fac-8576-0cc8d73841fb.mp4" - }, - { - "id": "4b6f6480-f89b-4190-ad8c-81441e8708df", - "created_date": "2024-07-25 07:29:45.348018", - "last_modified_date": "2024-07-25 07:29:45.348018", - "version": 0, - "url": "https://ge.xhamster.com/videos/und-wieder-eine-total-versaute-familie-episode-04-xhY03LM", - "review": 0, - "should_download": 0, - "title": "Und Wieder Eine Total Versaute Familie - Episode 04 | xHamster", - "file_name": "Und wieder eine total versaute Familie - Episode #04 [xhY03LM].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4b6f6480-f89b-4190-ad8c-81441e8708df.mp4" - }, - { - "id": "4b8e3f08-3fdc-4857-809e-66add30c9ba8", - "created_date": "2024-07-25 07:29:47.152544", - "last_modified_date": "2024-07-25 07:29:47.152544", - "version": 0, - "url": "https://ge.xhamster.com/videos/nudist-sex-at-the-lake-7059674", - "review": 0, - "should_download": 0, - "title": "FKK-Sex am See | xHamster", - "file_name": "FKK-Sex am See [7059674].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4b8e3f08-3fdc-4857-809e-66add30c9ba8.mp4" - }, - { - "id": "4b95d8c6-f1ba-4371-be6c-2e88aefba1ff", - "created_date": "2024-08-28 23:21:54.373625", - "last_modified_date": "2024-08-28 23:21:54.373625", - "version": 0, - "url": "https://ge.xhamster.com/videos/naked-stranger-vhs-1987-xhG0GkR", - "review": 0, - "should_download": 0, - "title": "Nackter Fremder (vhs 1987) | xHamster", - "file_name": "Nackter Fremder (vhs 1987) [xhG0GkR].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/4b95d8c6-f1ba-4371-be6c-2e88aefba1ff.mp4" - }, - { - "id": "4ba4cf6a-76a1-459d-a9ca-36da83822c1e", - "created_date": "2024-07-25 07:29:47.220900", - "last_modified_date": "2024-07-25 07:29:47.220900", - "version": 0, - "url": "https://ge.xhamster.com/videos/lezioni-private-1975-5904006", - "review": 0, - "should_download": 0, - "title": "Lezioni Private 1975: Free MILF Porn Video 46 | xHamster", - "file_name": "Lezioni private (1975) [5904006].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4ba4cf6a-76a1-459d-a9ca-36da83822c1e.mp4" - }, - { - "id": "4bc31b4d-2926-4c4b-876a-f86f38078b52", - "created_date": "2024-07-25 07:29:44.582625", - "last_modified_date": "2024-07-25 07:29:44.582625", - "version": 0, - "url": "https://ge.xhamster.com/videos/rodox-boutique-voyeur-5572118", - "review": 0, - "should_download": 0, - "title": "Rodox Boutique Voyeur, Free Threesome Porn 4e | xHamster", - "file_name": "Rodox Boutique Voyeur [5572118].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4bc31b4d-2926-4c4b-876a-f86f38078b52.mp4" - }, - { - "id": "4be5562e-2dfe-468e-8763-3e9d945de656", - "created_date": "2024-07-25 07:29:46.023061", - "last_modified_date": "2024-07-25 07:29:46.023061", - "version": 0, - "url": "https://ge.xhamster.com/videos/outdoor-group-sex-party-6986638", - "review": 0, - "should_download": 0, - "title": "Outdoor-Gruppensex-Party | xHamster", - "file_name": "Outdoor-Gruppensex-Party [6986638].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4be5562e-2dfe-468e-8763-3e9d945de656.mp4" - }, - { - "id": "4bf4580b-6f16-434d-a359-4ca52ec5249c", - "created_date": "2024-07-25 07:29:46.457639", - "last_modified_date": "2024-07-25 07:29:46.457639", - "version": 0, - "url": "https://ge.xhamster.com/videos/after-soccer-training-chubby-gets-to-play-with-their-balls-4371418", - "review": 0, - "should_download": 0, - "title": "Nach dem Fu\u00dfballtraining darf die Mollige mit ihren Eiern spielen | xHamster", - "file_name": "Nach dem Fu\u00dfballtraining darf die Mollige mit ihren Eiern spielen [4371418].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4bf4580b-6f16-434d-a359-4ca52ec5249c.mp4" - }, - { - "id": "4bfaac0a-422d-42dc-86e9-fc4526c74c73", - "created_date": "2024-07-25 07:29:46.038466", - "last_modified_date": "2024-07-25 07:29:46.038466", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-strokes-helping-my-horny-stepbro-fuck-my-bff-xhjCo7D", - "review": 0, - "should_download": 0, - "title": "Family Strokes - meinem geilen Stiefbruder helfen, meine beste Freundin zu ficken | xHamster", - "file_name": "Family Strokes - meinem geilen Stiefbruder helfen, meine beste Freundin zu ficken [xhjCo7D].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4bfaac0a-422d-42dc-86e9-fc4526c74c73.mp4" - }, - { - "id": "4c3fbb06-e64a-4946-b914-2d2e5d55275c", - "created_date": "2025-01-16 19:59:34.587285", - "last_modified_date": "2025-01-16 19:59:34.587290", - "version": 0, - "url": "https://ge.xhamster.com/videos/welcome-to-my-horny-family-hot-mmff-orgy-xh5k8Wq", - "review": 0, - "should_download": 0, - "title": "Willkommen in meiner geilen familie - hei\u00dfe MMFF-orgie | xHamster", - "file_name": "Willkommen in meiner geilen familie - hei\u00dfe MMFF-orgie [xh5k8Wq].mp4", - "path": null, - "cloud_link": "/data/media/4c3fbb06-e64a-4946-b914-2d2e5d55275c.mp4" - }, - { - "id": "4c44c381-724f-4704-8ee1-698dca8837e1", - "created_date": "2024-09-05 20:03:45.743745", - "last_modified_date": "2024-10-21 16:28:10.462000", - "version": 1, - "url": "https://ge.xhamster.com/videos/group-sex-around-a-table-13234089", - "review": 0, - "should_download": 0, - "title": "Gruppensex rund um einen Tisch | xHamster", - "file_name": "Gruppensex rund um einen Tisch [13234089].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/4c44c381-724f-4704-8ee1-698dca8837e1.mp4" - }, - { - "id": "4c80e94d-f562-420d-b3ca-6c96b7957ef7", - "created_date": "2024-07-25 07:29:48.029059", - "last_modified_date": "2024-07-25 07:29:48.029059", - "version": 0, - "url": "https://ge.xhamster.com/videos/nubilefilms-horny-blonde-makes-big-brother-cum-6701760", - "review": 0, - "should_download": 0, - "title": "Nubilefilms geile Blondine l\u00e4sst gro\u00dfen Bruder kommen | xHamster", - "file_name": "Nubilefilms geile Blondine l\u00e4sst gro\u00dfen Bruder kommen [6701760].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4c80e94d-f562-420d-b3ca-6c96b7957ef7.mp4" - }, - { - "id": "4cf6541d-145c-4871-b484-68cc25320010", - "created_date": "2024-07-25 07:29:44.794548", - "last_modified_date": "2024-07-25 07:29:44.794548", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsis-says-you-can-cum-on-my-tits-now-but-later-i-want-you-to-cum-in-me-xhxblyn", - "review": 0, - "should_download": 0, - "title": "Stiefschwester sagt, du kannst jetzt auf meine Titten kommen, aber sp\u00e4ter will ich, dass du in mir kommst! | xHamster", - "file_name": "Stiefschwester sagt, du kannst jetzt auf meine Titten kommen, aber sp\u00e4ter will ich, dass du in mir kommst! [xhxblyn].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4cf6541d-145c-4871-b484-68cc25320010.mp4" - }, - { - "id": "4cf871b5-7ffc-4a1c-a6c9-3adecdf0c806", - "created_date": "2024-07-25 07:29:45.471280", - "last_modified_date": "2024-07-25 07:29:45.471280", - "version": 0, - "url": "https://ge.xhamster.com/videos/taboo-2-xh2JMI3", - "review": 0, - "should_download": 0, - "title": "Tabu 2 | xHamster", - "file_name": "Tabu 2 [xh2JMI3].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4cf871b5-7ffc-4a1c-a6c9-3adecdf0c806.mp4" - }, - { - "id": "4d007852-38b0-4504-9fab-1da578ec6ac5", - "created_date": "2024-07-25 07:29:44.714860", - "last_modified_date": "2024-07-25 07:29:44.714860", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-bosss-wife-seduced-me-to-fuck-her-and-cum-over-her-huge-tits-xhq6oVf", - "review": 0, - "should_download": 0, - "title": "Die frau meines chefs hat mich verf\u00fchrt, sie zu ficken und \u00fcber ihre riesigen titten zu kommen | xHamster", - "file_name": "Die frau meines chefs hat mich verf\u00fchrt, sie zu ficken und \u00fcber ihre riesigen titten zu kommen [xhq6oVf].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/4d007852-38b0-4504-9fab-1da578ec6ac5.mp4" - }, - { - "id": "4d2daf75-83ae-40a7-bb49-5ba7a95ab23e", - "created_date": "2024-07-25 07:29:44.348820", - "last_modified_date": "2024-07-25 07:29:44.348820", - "version": 0, - "url": "https://ge.xhamster.com/videos/hot-pool-party-xhF4ZAT", - "review": 0, - "should_download": 0, - "title": "Hei\u00dfe Pool-Party | xHamster", - "file_name": "Hei\u00dfe Pool-Party [xhF4ZAT].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4d2daf75-83ae-40a7-bb49-5ba7a95ab23e.mp4" - }, - { - "id": "4d45852d-870c-4e95-bd9f-5ee68e0b9d3c", - "created_date": "2024-09-24 08:11:39.003025", - "last_modified_date": "2024-10-21 16:28:14.856000", - "version": 1, - "url": "https://ge.xhamster.com/videos/stepmother-stepson-love-affair-pt-1-of-3-cory-chase-xh19Maa", - "review": 0, - "should_download": 0, - "title": "Stiefmutter und Stiefsohn lieben Aff\u00e4re Teil 1 von 3 - Cory Chase | xHamster", - "file_name": "Stiefmutter und Stiefsohn lieben Aff\u00e4re Teil 1 von 3 - Cory Chase [xh19Maa].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/4d45852d-870c-4e95-bd9f-5ee68e0b9d3c.mp4" - }, - { - "id": "4db45293-df98-407b-8a09-8a1005486495", - "created_date": "2024-07-25 07:29:48.162056", - "last_modified_date": "2024-07-25 07:29:48.162056", - "version": 0, - "url": "https://ge.xhamster.com/videos/house-of-strange-desires-1985-14901424", - "review": 0, - "should_download": 0, - "title": "Haus der seltsamen W\u00fcnsche (1985) | xHamster", - "file_name": "Haus der seltsamen W\u00fcnsche (1985) [14901424].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4db45293-df98-407b-8a09-8a1005486495.mp4" - }, - { - "id": "4dfdab0c-4e48-4945-b96c-91f4faa873ea", - "created_date": "2024-07-25 07:29:44.757053", - "last_modified_date": "2024-07-25 07:29:44.757053", - "version": 0, - "url": "https://ge.xhamster.com/videos/sex-roulette-dice-teen-orgy-with-michelle-honeywells-girls-7973553", - "review": 0, - "should_download": 0, - "title": "Sex-Roulette, W\u00fcrfel-Teen-Orgie mit Michelle Honeywells M\u00e4dchen | xHamster", - "file_name": "Sex-Roulette, W\u00fcrfel-Teen-Orgie mit Michelle Honeywells M\u00e4dchen [7973553].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4dfdab0c-4e48-4945-b96c-91f4faa873ea.mp4" - }, - { - "id": "4e4feedb-f42f-4083-89c4-db080cab54f8", - "created_date": "2024-07-25 07:29:45.959302", - "last_modified_date": "2024-07-25 07:29:45.959302", - "version": 0, - "url": "https://ge.xhamster.com/videos/jade-seduces-random-strangers-by-the-lake-and-fucks-one-of-them-xhKhVKp", - "review": 0, - "should_download": 0, - "title": "Jade verf\u00fchrt zuf\u00e4llige Fremde am See und fickt einen von ihnen | xHamster", - "file_name": "Jade verf\u00fchrt zuf\u00e4llige Fremde am See und fickt einen von ihnen [xhKhVKp].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4e4feedb-f42f-4083-89c4-db080cab54f8.mp4" - }, - { - "id": "4e65fc4d-9dd4-490e-bbab-b62ef661679f", - "created_date": "2024-07-25 07:29:47.074178", - "last_modified_date": "2024-07-25 07:29:47.074178", - "version": 0, - "url": "https://ge.xhamster.com/videos/familie-matuschek-with-anja-rochus-9564619", - "review": 0, - "should_download": 0, - "title": "Familie Matuschek with Anja Rochus, Free Porn 8c | xHamster", - "file_name": "Familie Matuschek with anja rochus [9564619].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4e65fc4d-9dd4-490e-bbab-b62ef661679f.mp4" - }, - { - "id": "4e860d0a-26b9-41fe-b8de-e85fac4fa043", - "created_date": "2024-07-25 07:29:48.122098", - "last_modified_date": "2024-07-25 07:29:48.122098", - "version": 0, - "url": "https://ge.xhamster.com/videos/erotic-family-affair-1984-14544731", - "review": 0, - "should_download": 0, - "title": "Erotic Family Affair (1984) | xHamster", - "file_name": "Erotic Family Affair (1984) [14544731].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4e860d0a-26b9-41fe-b8de-e85fac4fa043.mp4" - }, - { - "id": "4ea85153-d4c2-4f94-8d2f-fa58e26d76e3", - "created_date": "2024-07-25 07:29:47.251073", - "last_modified_date": "2024-07-25 07:29:47.251073", - "version": 0, - "url": "https://ge.xhamster.com/videos/college-teens-enjoy-cockriding-and-sucking-12333921", - "review": 0, - "should_download": 0, - "title": "College-Teenager genie\u00dfen Cockriding und Lutschen | xHamster", - "file_name": "College-Teenager genie\u00dfen Cockriding und Lutschen [12333921].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4ea85153-d4c2-4f94-8d2f-fa58e26d76e3.mp4" - }, - { - "id": "4ea98a2f-cc7c-4ef4-a84a-43ce5e35bb15", - "created_date": "2024-07-25 07:29:44.846490", - "last_modified_date": "2024-07-25 07:29:44.846490", - "version": 0, - "url": "https://ge.xhamster.com/videos/horny-hot-housewives-go-fishing-for-cum-1785321", - "review": 0, - "should_download": 0, - "title": "Geile hei\u00dfe Hausfrauen fischen auf Sperma | xHamster", - "file_name": "Geile hei\u00dfe Hausfrauen fischen auf Sperma [1785321].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4ea98a2f-cc7c-4ef4-a84a-43ce5e35bb15.mp4" - }, - { - "id": "4ebce3a1-a547-47dd-bfe7-c48767dbb0d8", - "created_date": "2024-07-25 07:29:45.185216", - "last_modified_date": "2024-07-25 07:29:45.185216", - "version": 0, - "url": "https://ge.xhamster.com/videos/mature-mom-gets-even-with-son-by-fucking-his-best-friend-xhFquSd", - "review": 0, - "should_download": 0, - "title": "REIFE MUTTER bekommt sogar mit sohn, indem sie seinen besten freund fickt! | xHamster", - "file_name": "REIFE MUTTER bekommt sogar mit sohn, indem sie seinen besten freund fickt! [xhFquSd].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/4ebce3a1-a547-47dd-bfe7-c48767dbb0d8.mp4" - }, - { - "id": "4f17bde6-7e09-4a34-b541-9e12b386349f", - "created_date": "2024-07-25 07:29:48.179932", - "last_modified_date": "2024-07-25 07:29:48.179932", - "version": 0, - "url": "https://ge.xhamster.com/videos/group-sex-of-students-at-lake-1947714", - "review": 0, - "should_download": 0, - "title": "Gruppensex von Studenten am See | xHamster", - "file_name": "Gruppensex von Studenten am See [1947714].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4f17bde6-7e09-4a34-b541-9e12b386349f.mp4" - }, - { - "id": "4f1a7a2c-e446-420f-9278-a88081c94a6b", - "created_date": "2024-07-25 07:29:47.745312", - "last_modified_date": "2024-07-25 07:29:47.745312", - "version": 0, - "url": "https://ge.xhamster.com/videos/chubby-french-milf-mylene-agrees-to-a-threesome-but-not-a-foursome-xhEwin7", - "review": 0, - "should_download": 0, - "title": "Dreier war OK f\u00fcr Sie, Vierer war dann doch zu viel | xHamster", - "file_name": "Dreier war OK f\u00fcr Sie, Vierer war dann doch zu viel [xhEwin7].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4f1a7a2c-e446-420f-9278-a88081c94a6b.mp4" - }, - { - "id": "4f3553c8-43f8-4e63-b35f-12ef5e67c609", - "created_date": "2024-07-25 07:29:46.230470", - "last_modified_date": "2024-07-25 07:29:46.230470", - "version": 0, - "url": "https://ge.xhamster.com/videos/im-obsessed-with-my-ginger-stepdaughters-perfect-breasts-xhvBAB6", - "review": 0, - "should_download": 0, - "title": "Ich bin besessen von den perfekten Br\u00fcsten meiner rothaarigen Stieftochter | xHamster", - "file_name": "Ich bin besessen von den perfekten Br\u00fcsten meiner rothaarigen Stieftochter [xhvBAB6].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4f3553c8-43f8-4e63-b35f-12ef5e67c609.mp4" - }, - { - "id": "4f5dfb73-7dc6-4980-ba39-3323f40d4f43", - "created_date": "2024-07-25 07:29:48.184831", - "last_modified_date": "2024-07-25 07:29:48.184831", - "version": 0, - "url": "https://ge.xhamster.com/videos/araceli-looks-for-a-guy-to-fuck-around-a-nude-beach-xh2cW8k", - "review": 0, - "should_download": 0, - "title": "Araceli sucht nach einem Typen, der an einem FKK-Strand fickt | xHamster", - "file_name": "Araceli sucht nach einem Typen, der an einem FKK-Strand fickt [xh2cW8k].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4f5dfb73-7dc6-4980-ba39-3323f40d4f43.mp4" - }, - { - "id": "4fa0e220-ca8c-44d8-a709-a1144880d061", - "created_date": "2024-10-21 15:08:43.545288", - "last_modified_date": "2024-10-21 16:28:19.587000", - "version": 1, - "url": "https://ge.xhamster.com/videos/first-sandwich-xh1ci0h", - "review": 0, - "should_download": 0, - "title": "Erstes Sandwich | xHamster", - "file_name": "Erstes Sandwich [xh1ci0h].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/4fa0e220-ca8c-44d8-a709-a1144880d061.mp4" - }, - { - "id": "4fc46abd-abfb-49d2-9b07-e2d234a25a38", - "created_date": "2024-07-25 07:29:48.158533", - "last_modified_date": "2024-07-25 07:29:48.158533", - "version": 0, - "url": "https://ge.xhamster.com/videos/hot-teacher-double-anal-stormy-gale-12710987", - "review": 0, - "should_download": 0, - "title": "Hei\u00dfe Lehrerin, doppelter analer st\u00fcrmischer Sturm | xHamster", - "file_name": "Hei\u00dfe Lehrerin, doppelter analer st\u00fcrmischer Sturm [12710987].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4fc46abd-abfb-49d2-9b07-e2d234a25a38.mp4" - }, - { - "id": "4fcabce2-8cb3-4f35-8431-849252f8a29b", - "created_date": "2024-07-25 07:29:47.790293", - "last_modified_date": "2024-07-25 07:29:47.790293", - "version": 0, - "url": "https://ge.xhamster.com/videos/reality-kings-euro-sex-parties-sharing-and-caring-tina-9798051", - "review": 0, - "should_download": 0, - "title": "Reality - K\u00f6nige - Euro - Sexpartys - Teilen und F\u00fcrsorge - Tina | xHamster", - "file_name": "Reality - K\u00f6nige - Euro - Sexpartys - Teilen und F\u00fcrsorge - Tina [9798051].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4fcabce2-8cb3-4f35-8431-849252f8a29b.mp4" - }, - { - "id": "4ff668c7-52ab-4a21-bdef-bc461e27b34f", - "created_date": "2024-07-25 07:29:46.702222", - "last_modified_date": "2024-07-25 07:29:46.702222", - "version": 0, - "url": "https://ge.xhamster.com/videos/schweine-priester-2-die-beichte-11653992", - "review": 0, - "should_download": 0, - "title": "Schweine Priester 2 - Die Beichte, Free Porn 58 | xHamster", - "file_name": "Schweine Priester 2 - Die Beichte [11653992].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/4ff668c7-52ab-4a21-bdef-bc461e27b34f.mp4" - }, - { - "id": "4ffedc25-db73-4158-ae9a-f6b60521032c", - "created_date": "2024-10-07 20:47:56.423927", - "last_modified_date": "2024-10-21 16:28:26.753000", - "version": 1, - "url": "https://ge.xhamster.com/videos/ich-bin-jung-und-brauche-das-geld-nr-8-episode-3-xhmAfyy", - "review": 0, - "should_download": 0, - "title": "Ich Bin Jung - Und Brauche Das Geld Nr 8 - Episode 3 | xHamster", - "file_name": "Ich bin jung - und brauche das geld Nr.8 - Episode 3 [xhmAfyy].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/4ffedc25-db73-4158-ae9a-f6b60521032c.mp4" - }, - { - "id": "50029ef3-e276-4022-a685-981f54c6493e", - "created_date": "2024-07-25 07:29:46.706778", - "last_modified_date": "2024-07-25 07:29:46.706778", - "version": 0, - "url": "https://ge.xhamster.com/videos/kuken-3-episode-6-xhdmel3", - "review": 0, - "should_download": 0, - "title": "Kuken 3 - Episode 6: Free Big Cock Porn Video bf | xHamster", - "file_name": "KUKEN 3 - Episode 6 [xhdmel3].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/50029ef3-e276-4022-a685-981f54c6493e.mp4" - }, - { - "id": "5019cbfb-aa96-4efc-a929-969deb0ecda5", - "created_date": "2024-07-25 07:29:45.001056", - "last_modified_date": "2024-07-25 07:29:45.001056", - "version": 0, - "url": "https://ge.xhamster.com/videos/amazing-horny-wife-rent-pussy-fucked-by-husbands-friend-xhd70dc", - "review": 0, - "should_download": 0, - "title": "Erstaunliche geile Ehefrau Miete Muschi von Freund ihres Mannes gefickt | xHamster", - "file_name": "Erstaunliche geile Ehefrau Miete Muschi von Freund ihres Mannes gefickt [xhd70dc].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5019cbfb-aa96-4efc-a929-969deb0ecda5.mp4" - }, - { - "id": "50b175e7-8aee-4b74-bbde-0f8ab5fa4548", - "created_date": "2024-07-25 07:29:47.280052", - "last_modified_date": "2024-07-25 07:29:47.280052", - "version": 0, - "url": "https://ge.xhamster.com/videos/if-the-2-of-you-need-to-fuck-something-it-better-be-me-swapmom-cory-chase-demands-swap-family-s7-e2-xhVjuur", - "review": 0, - "should_download": 0, - "title": "\"Wenn die 2 von dir etwas ficken muss, bin ich besser ich!\" Swapmom Cory Chase verlangt den tausch der familie - S7: E2 | xHamster", - "file_name": "\uff02Wenn die 2 von dir etwas ficken muss, bin ich besser ich!\uff02 Swapmom Cory Chase verlangt den tausch der familie - S7\uff1a E2 [xhVjuur].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/50b175e7-8aee-4b74-bbde-0f8ab5fa4548.mp4" - }, - { - "id": "50b99054-5c7f-445b-8a34-d89329723e35", - "created_date": "2024-07-25 07:29:48.132684", - "last_modified_date": "2024-07-25 07:29:48.132684", - "version": 0, - "url": "https://ge.xhamster.com/videos/slutty-stepsister-scarlet-skies-moans-omg-i-want-every-fucking-inch-of-you-xhZfXLq", - "review": 0, - "should_download": 0, - "title": "Die versaute Stiefschwester Scarlet Skies st\u00f6hnt, \"omg, ich will jeden verdammten Zoll von dir\" | xHamster", - "file_name": "Die versaute Stiefschwester Scarlet Skies st\u00f6hnt, \uff02omg, ich will jeden verdammten Zoll von dir\uff02 [xhZfXLq].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/50b99054-5c7f-445b-8a34-d89329723e35.mp4" - }, - { - "id": "50c9828a-c9a0-4155-b331-54cf834c35fd", - "created_date": "2025-01-19 13:42:35.568910", - "last_modified_date": "2025-01-19 13:42:35.568916", - "version": 0, - "url": "https://ge.xhamster.com/videos/busty-step-moms-catch-their-respective-stepsons-masturbating-help-them-release-the-sexual-tension-xhxyTon", - "review": 0, - "should_download": 0, - "title": "Vollbusige stiefmutter erwischen ihre jeweiligen stiefsohn beim masturbieren und helfen ihnen, die sexuelle spannung zu l\u00f6sen | xHamster", - "file_name": "Vollbusige stiefmutter erwischen ihre jeweiligen stiefsohn beim masturbieren und helfen ihnen, die sexuelle spannung zu l\u00f6sen [xhxyTon].mp4", - "path": null, - "cloud_link": null - }, - { - "id": "5121c078-2917-4115-b879-2b57efdfdef6", - "created_date": "2024-07-25 07:29:48.155011", - "last_modified_date": "2024-07-25 07:29:48.155011", - "version": 0, - "url": "https://ge.xhamster.com/videos/deutschland-secrets-part-04-xh34sWp", - "review": 0, - "should_download": 0, - "title": "Deutschland Secrets - Part 04, Free European Porn Video 26 | xHamster", - "file_name": "Deutschland Secrets!!! - part #04 [xh34sWp].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5121c078-2917-4115-b879-2b57efdfdef6.mp4" - }, - { - "id": "5123d7ee-8d59-432f-bb15-582661e2b79a", - "created_date": "2024-07-25 07:29:45.888565", - "last_modified_date": "2024-07-25 07:29:45.888565", - "version": 0, - "url": "https://ge.xhamster.com/videos/sex-in-the-office-for-mya-diamond-xhagKYd", - "review": 0, - "should_download": 0, - "title": "Sex im B\u00fcro f\u00fcr Mya Diamond | xHamster", - "file_name": "Sex im B\u00fcro f\u00fcr Mya Diamond [xhagKYd].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5123d7ee-8d59-432f-bb15-582661e2b79a.mp4" - }, - { - "id": "517e6d4a-c0ab-4f6f-aeb9-459f0d80d885", - "created_date": "2024-07-25 07:29:46.607308", - "last_modified_date": "2024-07-25 07:29:46.607308", - "version": 0, - "url": "https://ge.xhamster.com/videos/nurumassage-chanel-preston-loves-giving-spicy-happy-endings-xh4rG7Q", - "review": 0, - "should_download": 0, - "title": "In Nurumassage liebt Chanel Preston w\u00fcrzige Happy Ends | xHamster", - "file_name": "In Nurumassage liebt Chanel Preston w\u00fcrzige Happy Ends [xh4rG7Q].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/517e6d4a-c0ab-4f6f-aeb9-459f0d80d885.mp4" - }, - { - "id": "5207b7eb-e076-4789-9257-002b888a4d0e", - "created_date": "2024-07-25 07:29:46.841301", - "last_modified_date": "2024-07-25 07:29:46.841301", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepdad-fucks-me-xhkddTh", - "review": 0, - "should_download": 0, - "title": "Stiefvater fickt mich | xHamster", - "file_name": "Stiefvater fickt mich [xhkddTh].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5207b7eb-e076-4789-9257-002b888a4d0e.mp4" - }, - { - "id": "521d8d21-31c7-4f56-9809-497d2895b665", - "created_date": "2024-07-25 07:29:46.764390", - "last_modified_date": "2024-07-25 07:29:46.764390", - "version": 0, - "url": "https://ge.xhamster.com/videos/sex-party-with-eva-kleber-9786674", - "review": 0, - "should_download": 0, - "title": "Sexparty mit Eva Kleber | xHamster", - "file_name": "Sexparty mit Eva Kleber [9786674].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/521d8d21-31c7-4f56-9809-497d2895b665.mp4" - }, - { - "id": "52b99664-d337-43ec-bfa5-ebcd3cdb731d", - "created_date": "2024-07-25 07:29:45.419222", - "last_modified_date": "2024-07-25 07:29:45.419222", - "version": 0, - "url": "https://ge.xhamster.com/videos/real-slut-party-jynx-maze-three-to-one-advantage-mofos-10366980", - "review": 0, - "should_download": 0, - "title": "Echte Schlampenparty - Jynx Labyrinth - drei zu einem Vorteil - Mofos | xHamster", - "file_name": "Echte Schlampenparty - Jynx Labyrinth - drei zu einem Vorteil - Mofos [10366980].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/52b99664-d337-43ec-bfa5-ebcd3cdb731d.mp4" - }, - { - "id": "52c70ddc-480b-472d-9c21-d4a629433db7", - "created_date": "2024-07-25 07:29:46.273594", - "last_modified_date": "2024-07-25 07:29:46.273594", - "version": 0, - "url": "https://ge.xhamster.com/videos/smart-guy-proposed-pretty-babe-to-hang-around-him-in-the-pool-one-hot-summer-day-xhzLX8b", - "review": 0, - "should_download": 0, - "title": "Kluger Typ schlug ein h\u00fcbsches Sch\u00e4tzchen vor, sich an einem hei\u00dfen Sommertag um ihn im Pool zu h\u00e4ngen | xHamster", - "file_name": "Kluger Typ schlug ein h\u00fcbsches Sch\u00e4tzchen vor, sich an einem hei\u00dfen Sommertag um ihn im Pool zu h\u00e4ngen [xhzLX8b].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/52c70ddc-480b-472d-9c21-d4a629433db7.mp4" - }, - { - "id": "52de336f-c448-4e44-9a8f-e4dc46806f0e", - "created_date": "2024-07-25 07:29:46.453662", - "last_modified_date": "2024-07-25 07:29:46.453662", - "version": 0, - "url": "https://ge.xhamster.com/videos/me-and-my-stepdad-fuck-while-mom-is-away-full-movie-xhgdB4e", - "review": 0, - "should_download": 0, - "title": "Ich und mein Stiefvater ficken, w\u00e4hrend Mutter weg ist - kompletter Film | xHamster", - "file_name": "Ich und mein Stiefvater ficken, w\u00e4hrend Mutter weg ist - kompletter Film [xhgdB4e].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/52de336f-c448-4e44-9a8f-e4dc46806f0e.mp4" - }, - { - "id": "534cf3a7-f96d-4b70-9c50-8c02cf01640b", - "created_date": "2024-07-25 07:29:45.602307", - "last_modified_date": "2024-07-25 07:29:45.602307", - "version": 0, - "url": "https://ge.xhamster.com/videos/sex-office-part-4-fucked-a-feminist-like-a-whore-xhf2Rne", - "review": 0, - "should_download": 0, - "title": "Sexb\u00fcro, teil 4. Eine feministin wie eine hure gefickt! | xHamster", - "file_name": "Sexb\u00fcro, teil 4. Eine feministin wie eine hure gefickt! [xhf2Rne].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/534cf3a7-f96d-4b70-9c50-8c02cf01640b.mp4" - }, - { - "id": "53709293-c7ea-4859-b27d-a1fac53af07f", - "created_date": "2024-07-25 07:29:47.714984", - "last_modified_date": "2024-07-25 07:29:47.714984", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepbro-assfucked-beauty-and-cum-in-mouth-after-deepthroat-xhFMldy", - "review": 0, - "should_download": 0, - "title": "Stiefbruder Arschfick, Sch\u00f6nheit und Sperma im Mund nach Halsfick | xHamster", - "file_name": "Stiefbruder Arschfick, Sch\u00f6nheit und Sperma im Mund nach Halsfick [xhFMldy].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/53709293-c7ea-4859-b27d-a1fac53af07f.mp4" - }, - { - "id": "53d746e5-03ff-4c86-b52b-7382e7781ab9", - "created_date": "2024-11-10 16:53:33.493311", - "last_modified_date": "2024-11-10 16:53:33.493311", - "version": 0, - "url": "https://ge.xhamster.com/videos/girls-in-heat-1979-xhhI01M", - "review": 0, - "should_download": 0, - "title": "Girls in Heat 1979 | xHamster", - "file_name": "Girls in Heat 1979 [xhhI01M].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/53d746e5-03ff-4c86-b52b-7382e7781ab9.mp4" - }, - { - "id": "53f5423d-59d0-46d1-a37c-d80b1d77d248", - "created_date": "2024-07-25 07:29:45.666460", - "last_modified_date": "2024-07-25 07:29:45.666460", - "version": 0, - "url": "https://ge.xhamster.com/videos/truth-or-dare-3-6186855", - "review": 0, - "should_download": 0, - "title": "Truth or Dare 3: Free Amateur HD Porn Video 89 | xHamster", - "file_name": "Truth or Dare 3 [6186855].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/53f5423d-59d0-46d1-a37c-d80b1d77d248.mp4" - }, - { - "id": "54227ce7-9428-4e2d-b4ae-77bddf17ce52", - "created_date": "2024-07-25 07:29:44.886648", - "last_modified_date": "2024-07-25 07:29:44.886648", - "version": 0, - "url": "https://ge.xhamster.com/videos/paare-ueberkommt-die-lust-im-pornokino-in-berlin-1-6581727", - "review": 0, - "should_download": 0, - "title": "Paare Ueberkommt Die Lust Im Pornokino in Berlin 1: Porn 3a | xHamster", - "file_name": "Paare ueberkommt die Lust im Pornokino in Berlin 1 [6581727].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/54227ce7-9428-4e2d-b4ae-77bddf17ce52.mp4" - }, - { - "id": "54326e45-7267-4045-b77b-f8e9cb51a274", - "created_date": "2024-10-07 20:47:56.413263", - "last_modified_date": "2024-10-21 16:28:34.984000", - "version": 1, - "url": "https://ge.xhamster.com/videos/naughty-teen-girlfriend-home-anal-threesome-with-facials-9106304", - "review": 0, - "should_download": 0, - "title": "Freche Teen-Freundin zu Hause anal Dreier mit Gesichtsbesamungen | xHamster", - "file_name": "Freche Teen-Freundin zu Hause anal Dreier mit Gesichtsbesamungen [9106304].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/54326e45-7267-4045-b77b-f8e9cb51a274.mp4" - }, - { - "id": "545b0be8-167b-4d09-b2b9-7430213611b7", - "created_date": "2024-08-28 23:21:54.356286", - "last_modified_date": "2024-08-28 23:21:54.356286", - "version": 0, - "url": "https://ge.xhamster.com/videos/fucking-during-a-pool-party-is-so-much-fun-for-these-beautiful-teens-xhMzvsJ", - "review": 0, - "should_download": 0, - "title": "Ficken w\u00e4hrend einer Pool-Party macht so viel Spa\u00df f\u00fcr diese sch\u00f6nen Teenager | xHamster", - "file_name": "Ficken w\u00e4hrend einer Pool-Party macht so viel Spa\u00df f\u00fcr diese sch\u00f6nen Teenager [xhMzvsJ].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/545b0be8-167b-4d09-b2b9-7430213611b7.mp4" - }, - { - "id": "5472a49b-7bc5-4103-a5ce-6326911455ca", - "created_date": "2024-07-25 07:29:44.258863", - "last_modified_date": "2024-07-25 07:29:44.258863", - "version": 0, - "url": "https://ge.xhamster.com/videos/dads-dirty-movies-8-1981-2144857", - "review": 0, - "should_download": 0, - "title": "Papas schmutzige Filme 8 - 1981 | xHamster", - "file_name": "Papas schmutzige Filme 8 - 1981 [2144857].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5472a49b-7bc5-4103-a5ce-6326911455ca.mp4" - }, - { - "id": "54f2f917-95a9-4c44-ab51-5d31d722dcea", - "created_date": "2024-07-25 07:29:45.842919", - "last_modified_date": "2024-07-25 07:29:45.842919", - "version": 0, - "url": "https://ge.xhamster.com/videos/two-couple-have-group-sex-at-beach-xh0AQpR", - "review": 0, - "should_download": 0, - "title": "Zwei paare haben gruppensex am strand | xHamster", - "file_name": "Zwei paare haben gruppensex am strand [xh0AQpR].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/54f2f917-95a9-4c44-ab51-5d31d722dcea.mp4" - }, - { - "id": "551169a2-9e1e-4136-856c-b1588e9b1cc4", - "created_date": "2024-07-25 07:29:47.842916", - "last_modified_date": "2024-07-25 07:29:47.842916", - "version": 0, - "url": "https://ge.xhamster.com/videos/flying-acquaintances-1973-us-jamie-gillis-softcore-dvd-xhlILiu", - "review": 0, - "should_download": 0, - "title": "Fliegende Bekannte (1973, wir, Jamie Gillis, Softcore, DVD) | xHamster", - "file_name": "Fliegende Bekannte (1973, wir, Jamie Gillis, Softcore, DVD) [xhlILiu].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/551169a2-9e1e-4136-856c-b1588e9b1cc4.mp4" - }, - { - "id": "5563cb67-686c-4909-aeca-eb12ec0495d8", - "created_date": "2024-07-25 07:29:48.189497", - "last_modified_date": "2024-07-25 07:29:48.189497", - "version": 0, - "url": "https://ge.xhamster.com/videos/daddy4k-his-post-traumatic-stress-disorder-caused-the-wildest-sex-of-her-life-xhQTJKA", - "review": 0, - "should_download": 0, - "title": "DADDY4K. Seine posttraumatische Stressst\u00f6rung verursachte den wildesten sex ihres Lebens | xHamster", - "file_name": "DADDY4K. Seine posttraumatische Stressst\u00f6rung verursachte den wildesten sex ihres Lebens [xhQTJKA].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5563cb67-686c-4909-aeca-eb12ec0495d8.mp4" - }, - { - "id": "55649871-846b-4e76-950c-6b0f3e815b15", - "created_date": "2024-07-25 07:29:47.683167", - "last_modified_date": "2024-07-25 07:29:47.683167", - "version": 0, - "url": "https://ge.xhamster.com/videos/emily-willis-says-to-sky-oh-my-god-come-here-you-have-to-taste-your-stepbros-cock-s11-e12-xhxSBsf", - "review": 0, - "should_download": 0, - "title": "Emily Willis sagt zu Sky: \"Oh mein Gott, komm her, du musst den Schwanz deines Stiefbruders probieren\" - s11: e12 | xHamster", - "file_name": "Emily Willis sagt zu Sky\uff1a \uff02Oh mein Gott, komm her, du musst den Schwanz deines Stiefbruders probieren\uff02 - s11\uff1a e12 [xhxSBsf].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/55649871-846b-4e76-950c-6b0f3e815b15.mp4" - }, - { - "id": "5579981c-0b0e-4af8-a80d-f8f5a50081c8", - "created_date": "2024-07-25 07:29:46.088138", - "last_modified_date": "2024-07-25 07:29:46.088138", - "version": 0, - "url": "https://ge.xhamster.com/videos/get-out-of-my-room-you-perv-6036597", - "review": 0, - "should_download": 0, - "title": "Raus aus meinem Zimmer, du Perverser! | xHamster", - "file_name": "Raus aus meinem Zimmer, du Perverser! [6036597].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5579981c-0b0e-4af8-a80d-f8f5a50081c8.mp4" - }, - { - "id": "56453e6f-fd97-4f0e-bc58-4a23b7d23076", - "created_date": "2024-11-10 16:53:33.488529", - "last_modified_date": "2024-11-10 16:53:33.488529", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-swingers-10408941", - "review": 0, - "should_download": 0, - "title": "Familien-Swinger | xHamster", - "file_name": "Familien-Swinger [10408941].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/56453e6f-fd97-4f0e-bc58-4a23b7d23076.mp4" - }, - { - "id": "56598d06-e04b-4904-aa7e-8cec9dfdf0d6", - "created_date": "2024-07-25 07:29:47.881238", - "last_modified_date": "2024-07-25 07:29:47.881238", - "version": 0, - "url": "https://ge.xhamster.com/videos/pool-side-blowjob-orgy-1570838", - "review": 0, - "should_download": 0, - "title": "Pool-Seite Blowjob-Orgie | xHamster", - "file_name": "Pool-Seite Blowjob-Orgie [1570838].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/56598d06-e04b-4904-aa7e-8cec9dfdf0d6.mp4" - }, - { - "id": "56c6ec9c-880b-41bb-b82b-251b5c17682f", - "created_date": "2024-07-25 07:29:47.596870", - "last_modified_date": "2024-07-25 07:29:47.596870", - "version": 0, - "url": "https://ge.xhamster.com/videos/blonde-fucked-hard-in-a-boat-on-the-lake-three-guys-1918591", - "review": 0, - "should_download": 0, - "title": "Blondine hart gefickt in einem Boot auf dem See drei Typen | xHamster", - "file_name": "Blondine hart gefickt in einem Boot auf dem See drei Typen [1918591].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/56c6ec9c-880b-41bb-b82b-251b5c17682f.mp4" - }, - { - "id": "572a6e1b-6b59-4d15-b951-37b9eca47686", - "created_date": "2024-07-25 07:29:44.242011", - "last_modified_date": "2024-07-25 07:29:44.242011", - "version": 0, - "url": "https://ge.xhamster.com/videos/if-i-get-naked-you-have-to-get-naked-too-scarlett-hampton-tells-swapbro-s6-e3-xhnseD1", - "review": 0, - "should_download": 0, - "title": "\"Wenn ich mich nackig mache, musst du dich auch nackt machen\", sagt Scarlett Hampton zu Swapbro -s6: e3 | xHamster", - "file_name": "\uff02Wenn ich mich nackig mache, musst du dich auch nackt machen\uff02, sagt Scarlett Hampton zu Swapbro -s6\uff1a e3 [xhnseD1].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/572a6e1b-6b59-4d15-b951-37b9eca47686.mp4" - }, - { - "id": "5737b039-4ebd-4a95-b9b7-5d5cb6b55bf9", - "created_date": "2024-07-25 07:29:47.182974", - "last_modified_date": "2024-07-25 07:29:47.182974", - "version": 0, - "url": "https://ge.xhamster.com/videos/bumsfidele-hochzeitsnacht-5805485", - "review": 0, - "should_download": 0, - "title": "Bumsfidele Hochzeitsnacht, Free Vintage Porn 10 | xHamster", - "file_name": "Bumsfidele Hochzeitsnacht [5805485].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5737b039-4ebd-4a95-b9b7-5d5cb6b55bf9.mp4" - }, - { - "id": "57ad263d-b58f-40b6-a4ea-7bc6fb18e54d", - "created_date": "2024-07-25 07:29:44.779371", - "last_modified_date": "2024-07-25 07:29:44.779371", - "version": 0, - "url": "https://ge.xhamster.com/videos/young-son-shares-first-girlfriend-with-his-stepfather-xhKFmcc", - "review": 0, - "should_download": 0, - "title": "Junger Sohn teilt erste Freundin mit seinem Stiefvater | xHamster", - "file_name": "Junger Sohn teilt erste Freundin mit seinem Stiefvater [xhKFmcc].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/57ad263d-b58f-40b6-a4ea-7bc6fb18e54d.mp4" - }, - { - "id": "57bf1fdc-d16f-46f9-9342-5e0e6c6366aa", - "created_date": "2024-07-25 07:29:46.216935", - "last_modified_date": "2024-07-25 07:29:46.216935", - "version": 0, - "url": "https://ge.xhamster.desi/videos/stepsis-says-do-you-have-a-boner-xhCYUy5", - "review": 0, - "should_download": 0, - "title": "Stiefschwester sagt, hast du eine Latte ?! | xHamster", - "file_name": "Stiefschwester sagt, hast du eine Latte \uff1f! [xhCYUy5].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/57bf1fdc-d16f-46f9-9342-5e0e6c6366aa.mp4" - }, - { - "id": "584063e4-31bc-4e16-bfd1-8e19e8291769", - "created_date": "2024-07-25 07:29:46.870579", - "last_modified_date": "2024-07-25 07:29:46.870579", - "version": 0, - "url": "https://ge.xhamster.com/videos/clothed-british-babes-give-head-xhXnnMO", - "review": 0, - "should_download": 0, - "title": "Bekleidete britische Sch\u00e4tzchen blasen | xHamster", - "file_name": "Bekleidete britische Sch\u00e4tzchen blasen [xhXnnMO].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/584063e4-31bc-4e16-bfd1-8e19e8291769.mp4" - }, - { - "id": "585137a1-4abe-4c62-9277-14e4acf8bbfb", - "created_date": "2024-07-25 07:29:45.098347", - "last_modified_date": "2024-07-25 07:29:45.098347", - "version": 0, - "url": "https://ge.xhamster.com/videos/taboo-family-sex-on-the-boat-11799368", - "review": 0, - "should_download": 0, - "title": "Tabu, Familiensex auf dem Boot | xHamster", - "file_name": "Tabu, Familiensex auf dem Boot [11799368].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/585137a1-4abe-4c62-9277-14e4acf8bbfb.mp4" - }, - { - "id": "58b23a74-a62c-4e93-b219-7a4fca14beec", - "created_date": "2024-07-25 07:29:45.464227", - "last_modified_date": "2024-07-25 07:29:45.464227", - "version": 0, - "url": "https://ge.xhamster.com/videos/geschichten-2-blut-schande-10248071", - "review": 0, - "should_download": 0, - "title": "Geschichten 2- Blut Schande, Free Porn Video f8 | xHamster", - "file_name": "Geschichten 2- Blut Schande [10248071].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/58b23a74-a62c-4e93-b219-7a4fca14beec.mp4" - }, - { - "id": "58cef52e-ad57-4d72-9e26-963ba9f8c6af", - "created_date": "2024-07-25 07:29:46.119204", - "last_modified_date": "2024-07-25 07:29:46.119204", - "version": 0, - "url": "https://ge.xhamster.com/videos/brattysis-katie-kush-to-stepbro-i-want-you-to-fuck-my-pie-s29-e7-xhV9wAY", - "review": 0, - "should_download": 0, - "title": "Brattysis \u2013 Katie kush f\u00fcr stiefbruer, \"ich will, dass du meinen kuchen fickst\" - s29: e7 | xHamster", - "file_name": "Brattysis \u2013 Katie kush f\u00fcr stiefbruer, \uff02ich will, dass du meinen kuchen fickst\uff02 - s29\uff1a e7 [xhV9wAY].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/58cef52e-ad57-4d72-9e26-963ba9f8c6af.mp4" - }, - { - "id": "59022dd4-709a-45c2-9fe6-04f90dfd2d91", - "created_date": "2024-07-25 07:29:47.141570", - "last_modified_date": "2024-07-25 07:29:47.141570", - "version": 0, - "url": "https://ge.xhamster.com/videos/party-has-three-lakefront-3459034", - "review": 0, - "should_download": 0, - "title": "Party hat drei am Seeufer | xHamster", - "file_name": "Party hat drei am Seeufer [3459034].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/59022dd4-709a-45c2-9fe6-04f90dfd2d91.mp4" - }, - { - "id": "591b5b7d-6afe-419e-8bf7-4af09527350a", - "created_date": "2024-07-25 07:29:46.550222", - "last_modified_date": "2024-07-25 07:29:46.550222", - "version": 0, - "url": "https://ge.xhamster.com/videos/step-moms-you-love-to-12799611", - "review": 0, - "should_download": 0, - "title": "Stiefmutter, die du liebst | xHamster", - "file_name": "Stiefmutter, die du liebst [12799611].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/591b5b7d-6afe-419e-8bf7-4af09527350a.mp4" - }, - { - "id": "5959b67b-96af-483f-888a-24539bc11751", - "created_date": "2024-07-25 07:29:47.942319", - "last_modified_date": "2024-07-25 07:29:47.942319", - "version": 0, - "url": "https://ge.xhamster.com/videos/vanilla-skye-gets-fucked-by-the-pool-2510051", - "review": 0, - "should_download": 0, - "title": "Vanilla Skye wird am Pool gefickt | xHamster", - "file_name": "Vanilla Skye wird am Pool gefickt [2510051].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5959b67b-96af-483f-888a-24539bc11751.mp4" - }, - { - "id": "5959f5dc-8566-4512-a46e-676121b26783", - "created_date": "2024-07-25 07:29:47.956670", - "last_modified_date": "2024-07-25 07:29:47.956670", - "version": 0, - "url": "https://ge.xhamster.com/videos/sex-project-full-movie-xh7tnc6", - "review": 0, - "should_download": 0, - "title": "Sex Project - kompletter film | xHamster", - "file_name": "Sex Project - kompletter film [xh7tnc6].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5959f5dc-8566-4512-a46e-676121b26783.mp4" - }, - { - "id": "595b08a2-faa6-4341-a8b4-3904e61be2c8", - "created_date": "2024-07-25 07:29:44.954569", - "last_modified_date": "2024-07-25 07:29:44.954569", - "version": 0, - "url": "https://ge.xhamster.com/videos/father-and-stepson-share-a-hot-blonde-teen-xhC8J2w", - "review": 0, - "should_download": 0, - "title": "Vater und Stiefsohn teilen sich ein hei\u00dfes blondes Teenie | xHamster", - "file_name": "Vater und Stiefsohn teilen sich ein hei\u00dfes blondes Teenie [xhC8J2w].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/595b08a2-faa6-4341-a8b4-3904e61be2c8.mp4" - }, - { - "id": "597198cb-d2fc-42c7-8c61-2c3e85a50bd4", - "created_date": "2024-07-25 07:29:45.700003", - "last_modified_date": "2024-07-25 07:29:45.700003", - "version": 0, - "url": "https://ge.xhamster.com/videos/taboo-italians-my-family-xhZPkBr", - "review": 0, - "should_download": 0, - "title": "Tabu-Italiener - meine Familie | xHamster", - "file_name": "Tabu-Italiener - meine Familie [xhZPkBr].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/597198cb-d2fc-42c7-8c61-2c3e85a50bd4.mp4" - }, - { - "id": "59849c2f-cf06-4d44-abdd-f22359f77276", - "created_date": "2024-07-25 07:29:46.169936", - "last_modified_date": "2024-07-25 07:29:46.169936", - "version": 0, - "url": "https://ge.xhamster.com/videos/tushy-blair-williams-has-a-hot-anal-lesson-threesome-8456320", - "review": 0, - "should_download": 0, - "title": "Tushy Blair Williams hat einen hei\u00dfen Anal-Dreier | xHamster", - "file_name": "Tushy Blair Williams hat einen hei\u00dfen Anal-Dreier [8456320].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/59849c2f-cf06-4d44-abdd-f22359f77276.mp4" - }, - { - "id": "59e64453-8745-4547-8fb3-def3afeeeabe", - "created_date": "2024-07-25 07:29:48.136085", - "last_modified_date": "2024-07-25 07:29:48.136085", - "version": 0, - "url": "https://ge.xhamster.com/videos/garage-girls-1981-6110405", - "review": 0, - "should_download": 0, - "title": "Garage Girls (1981) | xHamster", - "file_name": "Garage Girls (1981) [6110405].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/59e64453-8745-4547-8fb3-def3afeeeabe.mp4" - }, - { - "id": "5a2e2dd3-3078-4e31-8042-4ffbb005239e", - "created_date": "2024-09-24 08:11:39.004921", - "last_modified_date": "2024-10-21 16:28:44.680000", - "version": 1, - "url": "https://ge.xhamster.com/videos/shared-wife-with-step-daddys-friends-12336214", - "review": 0, - "should_download": 0, - "title": "Geteilte Ehefrau mit Stiefvaters Freunden | xHamster", - "file_name": "Geteilte Ehefrau mit Stiefvaters Freunden [12336214].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/5a2e2dd3-3078-4e31-8042-4ffbb005239e.mp4" - }, - { - "id": "5a326825-bab5-424b-a4f0-f3524f48b9d5", - "created_date": "2024-08-28 23:21:54.357799", - "last_modified_date": "2024-10-21 16:28:49.846000", - "version": 2, - "url": "https://ge.xhamster.com/videos/strip-poker-orgy-10775093", - "review": 0, - "should_download": 0, - "title": "Strip-Poker-Orgie | xHamster", - "file_name": "Strip-Poker-Orgie [10775093].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/5a326825-bab5-424b-a4f0-f3524f48b9d5.mp4" - }, - { - "id": "5a52e2dd-ad19-4ebf-9c01-1871d12a7150", - "created_date": "2024-07-25 07:29:45.854821", - "last_modified_date": "2024-07-25 07:29:45.854821", - "version": 0, - "url": "https://ge.xhamster.com/videos/sex-in-paradise-exploring-a-caribbean-island-and-having-sex-outdoors-beach-sex-outdoor-sex-swingers-teenage-couple-xhV0XcM", - "review": 0, - "should_download": 0, - "title": "Sex im Paradies, eine karibische Insel erkunden und Sex im Freien haben, Strandsex, Sex im Freien, Swinger-Teenager-Paar | xHamster", - "file_name": "Sex im Paradies, eine karibische Insel erkunden und Sex im Freien haben, Strandsex, Sex im Freien, Swinger-Teenager-Paar [xhV0XcM].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5a52e2dd-ad19-4ebf-9c01-1871d12a7150.mp4" - }, - { - "id": "5a7d4ee0-50f8-405e-8e15-99216b07ed32", - "created_date": "2024-07-25 07:29:45.112666", - "last_modified_date": "2024-07-25 07:29:45.112666", - "version": 0, - "url": "https://ge.xhamster.com/videos/american-family-secrets-chapter-05-xhuAfiH", - "review": 0, - "should_download": 0, - "title": "Amerikanische Familiengeheimnisse !!! - Kapitel # 05 | xHamster", - "file_name": "Amerikanische Familiengeheimnisse !!! - Kapitel # 05 [xhuAfiH].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5a7d4ee0-50f8-405e-8e15-99216b07ed32.mp4" - }, - { - "id": "5aa31eb9-c28d-4d64-9f02-9790d9af3f79", - "created_date": "2024-07-25 07:29:44.413437", - "last_modified_date": "2024-07-25 07:29:44.413437", - "version": 0, - "url": "https://ge.xhamster.com/videos/naturist-village-slutty-girl-feels-like-taking-a-strangers-cock-xhqLzCi", - "review": 0, - "should_download": 0, - "title": "Fkk village - ein versautes m\u00e4dchen f\u00fchlt sich, den schwanz eines fremden zu nehmen! | xHamster", - "file_name": "Fkk village - ein versautes m\u00e4dchen f\u00fchlt sich, den schwanz eines fremden zu nehmen! [xhqLzCi].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5aa31eb9-c28d-4d64-9f02-9790d9af3f79.mp4" - }, - { - "id": "5ab3147d-9bd8-4542-9691-46a97d0475cd", - "created_date": "2024-07-25 07:29:45.688814", - "last_modified_date": "2024-07-25 07:29:45.688814", - "version": 0, - "url": "https://ge.xhamster.com/videos/which-butthole-feels-better-stepmom-anal-challenge-abby-somers-xhryCzM", - "review": 0, - "should_download": 0, - "title": "Welches Arschloch f\u00fchlt sich besser an? Stiefmutter anal Herausforderung - Abby Somers | xHamster", - "file_name": "Welches Arschloch f\u00fchlt sich besser an\uff1f Stiefmutter anal Herausforderung - Abby Somers [xhryCzM].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5ab3147d-9bd8-4542-9691-46a97d0475cd.mp4" - }, - { - "id": "5b01bda7-be76-4a9e-8a2a-d5e233cfd95f", - "created_date": "2024-07-25 07:29:48.002746", - "last_modified_date": "2024-07-25 07:29:48.002746", - "version": 0, - "url": "https://ge.xhamster.com/videos/blindfolded-bride-gets-surprised-by-two-hard-cocks-at-once-xh6yaQD", - "review": 0, - "should_download": 0, - "title": "Die braut mit verbundenen augen wird von zwei harten schw\u00e4nze gleichzeitig \u00fcberrascht | xHamster", - "file_name": "Die braut mit verbundenen augen wird von zwei harten schw\u00e4nze gleichzeitig \u00fcberrascht [xh6yaQD].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5b01bda7-be76-4a9e-8a2a-d5e233cfd95f.mp4" - }, - { - "id": "5b1ddc83-ebf9-4b66-a069-b7b2e197d7d7", - "created_date": "2024-12-29 23:53:27.917609", - "last_modified_date": "2024-12-29 23:53:27.917609", - "version": 0, - "url": "https://ge.xhamster.com/videos/sex-spa-1984-12207796", - "review": 0, - "should_download": 0, - "title": "Sex Spa (1984) | xHamster", - "file_name": "Sex Spa (1984) [12207796].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/5b1ddc83-ebf9-4b66-a069-b7b2e197d7d7.mp4" - }, - { - "id": "5b34920c-6417-4b45-bcf1-de8c85f5d12d", - "created_date": "2024-07-25 07:29:47.077744", - "last_modified_date": "2025-01-03 11:56:26.627000", - "version": 1, - "url": "https://ge.xhamster.com/videos/the-family-magic-tea-turns-them-into-sluts-14174252", - "review": 0, - "should_download": 0, - "title": "Der magische Familientee macht sie zu Schlampen | xHamster", - "file_name": "Der magische Familientee macht sie zu Schlampen [14174252].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5b34920c-6417-4b45-bcf1-de8c85f5d12d.mp4" - }, - { - "id": "5b463837-1d9f-4f02-ade2-971add50ce45", - "created_date": "2024-07-25 07:29:47.979248", - "last_modified_date": "2024-09-06 09:40:21.347000", - "version": 1, - "url": "https://ge.xhamster.com/videos/my-friends-hot-mom-is-sara-jay-and-she-is-craving-some-young-cock-xhSoQ7o", - "review": 0, - "should_download": 0, - "title": "Die hei\u00dfe Mutter meiner Freundin ist Sara Jay und sie sehnt sich nach einem jungen Schwanz", - "file_name": "Die hei\u00dfe Mutter meiner Freundin ist Sara Jay und sie sehnt sich nach einem jungen Schwanz [xhSoQ7o].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5b463837-1d9f-4f02-ade2-971add50ce45.mp4" - }, - { - "id": "5b5c2827-0006-43de-a761-da5a01c86501", - "created_date": "2024-07-25 07:29:47.748803", - "last_modified_date": "2024-07-25 07:29:47.748803", - "version": 0, - "url": "https://ge.xhamster.com/videos/horny-couple-invites-hot-brunette-to-join-them-mp4-8598631", - "review": 0, - "should_download": 0, - "title": "Geiles Paar l\u00e4dt hei\u00dfe Br\u00fcnette ein, sich ihnen anzuschlie\u00dfen.mp4 | xHamster", - "file_name": "Geiles Paar l\u00e4dt hei\u00dfe Br\u00fcnette ein, sich ihnen anzuschlie\u00dfen.mp4 [8598631].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5b5c2827-0006-43de-a761-da5a01c86501.mp4" - }, - { - "id": "5b6dc84a-1989-4996-9c3e-48f36461337b", - "created_date": "2024-07-25 07:29:44.821483", - "last_modified_date": "2024-07-25 07:29:44.821483", - "version": 0, - "url": "https://ge.xhamster.com/videos/hot-german-mom-fucks-step-sons-friends-9538002", - "review": 0, - "should_download": 0, - "title": "Geile Mutti fickt die Freunde ihres Sohnes | xHamster", - "file_name": "Geile Mutti fickt die Freunde ihres Sohnes [9538002].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5b6dc84a-1989-4996-9c3e-48f36461337b.mp4" - }, - { - "id": "5bb6e870-903b-4179-bebf-3ad4b84a0a69", - "created_date": "2024-07-25 07:29:46.911750", - "last_modified_date": "2024-07-25 07:29:46.911750", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsis-says-dear-diary-i-bet-his-dick-gets-so-hard-xh20zjn", - "review": 0, - "should_download": 0, - "title": "Stiefschwester sagt, liebes Tagebuch, ich wette, sein Schwanz wird so hart! | xHamster", - "file_name": "Stiefschwester sagt, liebes Tagebuch, ich wette, sein Schwanz wird so hart! [xh20zjn].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5bb6e870-903b-4179-bebf-3ad4b84a0a69.mp4" - }, - { - "id": "5bc7490d-bd11-4dd9-801b-78b3ab4fd173", - "created_date": "2024-12-29 23:53:27.916641", - "last_modified_date": "2024-12-29 23:53:27.916641", - "version": 0, - "url": "https://ge.xhamster.com/videos/welcome-to-mommys-taboo-family-13073473", - "review": 0, - "should_download": 0, - "title": "Willkommen in Mamas Tabu-Familie | xHamster", - "file_name": "Willkommen in Mamas Tabu-Familie [13073473].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/5bc7490d-bd11-4dd9-801b-78b3ab4fd173.mp4" - }, - { - "id": "5c407737-f187-4f49-864c-df446b6b7054", - "created_date": "2024-07-25 07:29:47.371632", - "last_modified_date": "2024-07-25 07:29:47.371632", - "version": 0, - "url": "https://ge.xhamster.com/videos/dont-tell-mommy-please-2734429", - "review": 0, - "should_download": 0, - "title": "Sag es Mami bitte nicht | xHamster", - "file_name": "Sag es Mami bitte nicht [2734429].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5c407737-f187-4f49-864c-df446b6b7054.mp4" - }, - { - "id": "5c417968-7b72-4514-a391-f93c37d0a87a", - "created_date": "2024-07-25 07:29:45.433581", - "last_modified_date": "2024-07-25 07:29:45.433581", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-strokes-hot-stepsis-fucks-her-stepbro-in-the-shower-and-then-her-stepdad-fucks-her-harder-xhYdNOs", - "review": 0, - "should_download": 0, - "title": "Family Strokes - hei\u00dfe Stiefschwester fickt ihren Stiefbruder in der Dusche und dann fickt ihr Stiefvater sie h\u00e4rter | xHamster", - "file_name": "Family Strokes - hei\u00dfe Stiefschwester fickt ihren Stiefbruder in der Dusche und dann fickt ihr Stiefvater sie h\u00e4rter [xhYdNOs].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5c417968-7b72-4514-a391-f93c37d0a87a.mp4" - }, - { - "id": "5c9b629f-77e5-4ff2-ab3c-63d40658a915", - "created_date": "2024-07-25 07:29:44.606499", - "last_modified_date": "2024-07-25 07:29:44.606499", - "version": 0, - "url": "https://ge.xhamster.com/videos/german-taboo-7217495", - "review": 0, - "should_download": 0, - "title": "Deutsches Tabu | xHamster", - "file_name": "Deutsches Tabu [7217495].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5c9b629f-77e5-4ff2-ab3c-63d40658a915.mp4" - }, - { - "id": "5c9b787a-cc4f-408b-a890-4f558d7f4d26", - "created_date": "2024-07-25 07:29:47.398639", - "last_modified_date": "2024-07-25 07:29:47.398639", - "version": 0, - "url": "https://ge.xhamster.com/videos/sarah-and-friends-12-1991-sarah-young-german-dvd-rip-xhInACm", - "review": 0, - "should_download": 0, - "title": "Sarah und Freunde 12 (1991, Sarah Young, deutsch, DVD-Rip) | xHamster", - "file_name": "Sarah und Freunde 12 (1991, Sarah Young, deutsch, DVD-Rip) [xhInACm].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5c9b787a-cc4f-408b-a890-4f558d7f4d26.mp4" - }, - { - "id": "5c9f183e-e176-43e2-9518-5e26cf90bdc5", - "created_date": "2024-07-25 07:29:45.498483", - "last_modified_date": "2024-07-25 07:29:45.498483", - "version": 0, - "url": "https://ge.xhamster.com/videos/anal-threesome-at-the-beach-with-a-spanish-milf-and-a-busty-arab-girl-xh6Tvf0", - "review": 0, - "should_download": 0, - "title": "Analer dreier am strand mit einer spanischen MILf und einem vollbusigen arabischen m\u00e4dchen | xHamster", - "file_name": "Analer dreier am strand mit einer spanischen MILf und einem vollbusigen arabischen m\u00e4dchen [xh6Tvf0].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5c9f183e-e176-43e2-9518-5e26cf90bdc5.mp4" - }, - { - "id": "5d0fa5de-01d4-4240-b328-9386a1ef3e80", - "created_date": "2024-12-29 23:53:27.919442", - "last_modified_date": "2024-12-29 23:53:27.919442", - "version": 0, - "url": "https://ge.xhamster.com/videos/horny-household-part-3-stepdads-love-comforting-his-stepdaughter-with-his-big-dick-xhN5NxK9", - "review": 0, - "should_download": 0, - "title": "Geiler haushalt (teil 3): Stiefvaters liebe: tr\u00f6stet seine stieftochter mit seinem gro\u00dfen schwanz | xHamster", - "file_name": "Geiler haushalt (teil 3)\uff1a Stiefvaters liebe\uff1a tr\u00f6stet seine stieftochter mit seinem gro\u00dfen schwanz [xhN5NxK9].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/5d0fa5de-01d4-4240-b328-9386a1ef3e80.mp4" - }, - { - "id": "5d404b67-887f-476a-bab8-78fcad0ccdcb", - "created_date": "2024-07-25 07:29:47.001386", - "last_modified_date": "2024-07-25 07:29:47.001386", - "version": 0, - "url": "https://ge.xhamster.com/videos/wild-sex-on-the-beach-3763318", - "review": 0, - "should_download": 0, - "title": "Wilder Sex am Strand | xHamster", - "file_name": "Wilder Sex am Strand [3763318].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5d404b67-887f-476a-bab8-78fcad0ccdcb.mp4" - }, - { - "id": "5d7ed6f5-8617-4604-9d79-54793782dfed", - "created_date": "2024-07-25 07:29:44.551139", - "last_modified_date": "2024-07-25 07:29:44.551139", - "version": 0, - "url": "https://ge.xhamster.com/videos/heisse-braut-1989-dir-hans-billian-9976820", - "review": 0, - "should_download": 0, - "title": "Heisse Braut 1989 Dir Hans Billian, Free Porn 38 | xHamster", - "file_name": "Heisse Braut (1989) dir. Hans Billian [9976820].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5d7ed6f5-8617-4604-9d79-54793782dfed.mp4" - }, - { - "id": "5da6602a-e3a8-4de3-8737-0164c659f202", - "created_date": "2024-07-25 07:29:46.461603", - "last_modified_date": "2024-07-25 07:29:46.461603", - "version": 0, - "url": "https://ge.xhamster.com/videos/just-for-her-ass-xhuWt6Q", - "review": 0, - "should_download": 0, - "title": "Nur f\u00fcr ihren Arsch !!! | xHamster", - "file_name": "Nur f\u00fcr ihren Arsch !!! [xhuWt6Q].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5da6602a-e3a8-4de3-8737-0164c659f202.mp4" - }, - { - "id": "5da8c367-6908-4cce-9fad-2f0967c514f3", - "created_date": "2024-07-25 07:29:44.974236", - "last_modified_date": "2024-07-25 07:29:44.974236", - "version": 0, - "url": "https://ge.xhamster.com/videos/shy-teen-gets-used-by-stepdad-and-his-buddies-xhOZ7JQ", - "review": 0, - "should_download": 0, - "title": "Sch\u00fcchterner Teenager wird von Stiefvater und seinen Kumpels benutzt! | xHamster", - "file_name": "Sch\u00fcchterner Teenager wird von Stiefvater und seinen Kumpels benutzt! [xhOZ7JQ].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5da8c367-6908-4cce-9fad-2f0967c514f3.mp4" - }, - { - "id": "5daa7f54-2e86-4bfd-8b83-7910b8be9b01", - "created_date": "2024-12-29 23:53:27.889419", - "last_modified_date": "2024-12-29 23:53:27.889419", - "version": 0, - "url": "https://ge.xhamster.com/videos/frantic-fucking-featuring-patricia-rhomberg-xhFIqkG", - "review": 0, - "should_download": 0, - "title": "Hektisches Ficken mit Patricia Rhomberg | xHamster", - "file_name": "Hektisches Ficken mit Patricia Rhomberg [xhFIqkG].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/5daa7f54-2e86-4bfd-8b83-7910b8be9b01.mp4" - }, - { - "id": "5dd55acb-053b-4d69-a256-e9843390a574", - "created_date": "2024-09-24 08:11:39.005295", - "last_modified_date": "2024-10-21 16:28:56.160000", - "version": 1, - "url": "https://ge.xhamster.com/videos/amateur-cheating-fuck-while-calling-her-boyfriend-german-teen-nicky-foxx-xhUZmay", - "review": 0, - "should_download": 0, - "title": "Fremder fickt sie und Freund ist am Telefon - Deutsche Nicky-Foxx beim Hotel Date | xHamster", - "file_name": "Fremder fickt sie und Freund ist am Telefon - Deutsche Nicky-Foxx beim Hotel Date [xhUZmay].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/5dd55acb-053b-4d69-a256-e9843390a574.mp4" - }, - { - "id": "5de3213d-e226-474d-acfb-e21e5c75ee1e", - "created_date": "2024-07-25 07:29:45.575197", - "last_modified_date": "2024-07-25 07:29:45.575197", - "version": 0, - "url": "https://ge.xhamster.com/videos/big-tit-whorehouse-7811453", - "review": 0, - "should_download": 0, - "title": "Vollbusiges Haus mit dicken Titten | xHamster", - "file_name": "Vollbusiges Haus mit dicken Titten [7811453].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5de3213d-e226-474d-acfb-e21e5c75ee1e.mp4" - }, - { - "id": "5df6de1c-be0c-4528-8acf-f8c1cdb0ffa6", - "created_date": "2024-07-25 07:29:44.424890", - "last_modified_date": "2024-07-25 07:29:44.424890", - "version": 0, - "url": "https://ge.xhamster.com/videos/familiensunden-unter-deutschen-dachern-15-11280871", - "review": 0, - "should_download": 0, - "title": "Familiensunden Unter Deutschen Dachern 15: Free Porn 48 | xHamster", - "file_name": "FAMILIENSUNDEN UNTER DEUTSCHEN DACHERN 15 [11280871].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5df6de1c-be0c-4528-8acf-f8c1cdb0ffa6.mp4" - }, - { - "id": "5e314554-b1c0-4e02-9da0-ed1db040109c", - "created_date": "2024-07-25 07:29:44.559139", - "last_modified_date": "2024-07-25 07:29:44.559139", - "version": 0, - "url": "https://ge.xhamster.com/videos/we-fuck-because-we-care-for-each-other-xhrMO3X", - "review": 0, - "should_download": 0, - "title": "Wir ficken, weil wir uns umeinander k\u00fcmmern | xHamster", - "file_name": "Wir ficken, weil wir uns umeinander k\u00fcmmern [xhrMO3X].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5e314554-b1c0-4e02-9da0-ed1db040109c.mp4" - }, - { - "id": "5e4bdcdb-fc0c-4d5a-9657-93cbad398aba", - "created_date": "2024-07-25 07:29:46.226481", - "last_modified_date": "2024-07-25 07:29:46.226481", - "version": 0, - "url": "https://ge.xhamster.com/videos/dasdys-home-3-full-movie-8509530", - "review": 0, - "should_download": 0, - "title": "Dasdys Zuhause 3, kompletter Film | xHamster", - "file_name": "Dasdys Zuhause 3, kompletter Film [8509530].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5e4bdcdb-fc0c-4d5a-9657-93cbad398aba.mp4" - }, - { - "id": "5e5a9fba-716d-41ed-b078-b7125af93a35", - "created_date": "2024-07-25 07:29:44.451331", - "last_modified_date": "2024-07-25 07:29:44.451331", - "version": 0, - "url": "https://ge.xhamster.com/videos/gesellschaftsspiele-1979-5765346", - "review": 0, - "should_download": 0, - "title": "Gesellschaftsspiele - 1979, Free Party Porn 75 | xHamster", - "file_name": "Gesellschaftsspiele - 1979 [5765346].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5e5a9fba-716d-41ed-b078-b7125af93a35.mp4" - }, - { - "id": "5e60f5d4-324b-4501-ad03-7758b3de745f", - "created_date": "2024-07-25 07:29:46.974788", - "last_modified_date": "2024-07-25 07:29:46.974788", - "version": 0, - "url": "https://ge.xhamster.com/videos/apartment-episode-3-xhZq3hQ", - "review": 0, - "should_download": 0, - "title": "Wohnung - Episode 3 | xHamster", - "file_name": "Wohnung - Episode 3 [xhZq3hQ].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5e60f5d4-324b-4501-ad03-7758b3de745f.mp4" - }, - { - "id": "5e706b05-2b91-474d-bd4a-92ee11578273", - "created_date": "2024-07-25 07:29:46.084657", - "last_modified_date": "2024-07-25 07:29:46.084657", - "version": 0, - "url": "https://ge.xhamster.com/videos/er-hat-mit-ihr-geilen-sex-auf-der-couch-xhDnyXN", - "review": 0, - "should_download": 0, - "title": "Er Hat Mit Ihr Geilen Sex Auf Der Couch, Porn 5f | xHamster", - "file_name": "Er hat mit Ihr geilen Sex auf der Couch [xhDnyXN].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5e706b05-2b91-474d-bd4a-92ee11578273.mp4" - }, - { - "id": "5e7a8581-9f05-409f-9aee-12c1896b22da", - "created_date": "2024-07-25 07:29:46.395500", - "last_modified_date": "2024-07-25 07:29:46.395500", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsister-and-bestie-give-cock-a-helping-hand-s4-e6-10523919", - "review": 0, - "should_download": 0, - "title": "Stiefschwester und Bestie helfen dem Schwanz s4: e6 | xHamster", - "file_name": "Stiefschwester und Bestie helfen dem Schwanz s4\uff1a e6 [10523919].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5e7a8581-9f05-409f-9aee-12c1896b22da.mp4" - }, - { - "id": "5e906951-4cd9-4fd4-a10f-27640c719259", - "created_date": "2024-07-25 07:29:47.447121", - "last_modified_date": "2024-07-25 07:29:47.447121", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsis-says-i-dont-know-if-youve-always-wanted-a-sister-or-if-you-just-want-to-stick-your-dick-in-my-tight-pussy-xhs2ZhF", - "review": 0, - "should_download": 0, - "title": "Stiefschwester sagt, ich wei\u00df nicht, ob du immer eine Schwester wolltest oder ob du deinen Schwanz einfach in meine enge Muschi stecken willst | xHamster", - "file_name": "Stiefschwester sagt, ich wei\u00df nicht, ob du immer eine Schwester wolltest oder ob du deinen Schwanz einfach in meine enge Muschi stecken willst [xhs2ZhF].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5e906951-4cd9-4fd4-a10f-27640c719259.mp4" - }, - { - "id": "5ea44d59-36ea-48c3-93e0-ca18fe52ce1b", - "created_date": "2024-07-25 07:29:45.816867", - "last_modified_date": "2024-07-25 07:29:45.816867", - "version": 0, - "url": "https://ge.xhamster.com/videos/sisters-1979-10432209", - "review": 0, - "should_download": 0, - "title": "Schwestern (1979) | xHamster", - "file_name": "Schwestern (1979) [10432209].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5ea44d59-36ea-48c3-93e0-ca18fe52ce1b.mp4" - }, - { - "id": "5f4a4cf8-d0f6-47e6-9767-7f950ca03ad9", - "created_date": "2024-07-25 07:29:46.366257", - "last_modified_date": "2024-07-25 07:29:46.366257", - "version": 0, - "url": "https://ge.xhamster.com/videos/his-dick-is-huge-i-just-want-to-see-it-tough-love-3some-13375985", - "review": 0, - "should_download": 0, - "title": "Sein Schwanz ist riesig, ich will es nur sehen - harte Liebe, Dreier | xHamster", - "file_name": "Sein Schwanz ist riesig, ich will es nur sehen - harte Liebe, Dreier [13375985].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5f4a4cf8-d0f6-47e6-9767-7f950ca03ad9.mp4" - }, - { - "id": "5f4f8175-93b7-4235-98d3-3d7c223bf587", - "created_date": "2024-07-25 07:29:47.206268", - "last_modified_date": "2024-07-25 07:29:47.206268", - "version": 0, - "url": "https://ge.xhamster.com/videos/hot-group-sex-on-a-public-spanish-beach-xhYvrzR", - "review": 0, - "should_download": 0, - "title": "Hei\u00dfer Gruppensex an einem \u00f6ffentlichen spanischen Strand | xHamster", - "file_name": "Hei\u00dfer Gruppensex an einem \u00f6ffentlichen spanischen Strand [xhYvrzR].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5f4f8175-93b7-4235-98d3-3d7c223bf587.mp4" - }, - { - "id": "5f53107e-4a25-45fd-abd1-c00b075833f2", - "created_date": "2024-07-25 07:29:44.760616", - "last_modified_date": "2024-07-25 07:29:44.760616", - "version": 0, - "url": "https://ge.xhamster.com/videos/dare-ring-game-11-xh0wXh0", - "review": 0, - "should_download": 0, - "title": "Dare-Ring-Spiel 11 | xHamster", - "file_name": "Dare-Ring-Spiel 11 [xh0wXh0].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5f53107e-4a25-45fd-abd1-c00b075833f2.mp4" - }, - { - "id": "5f6e1fc0-2430-41b0-813a-a53ed494a464", - "created_date": "2024-07-25 07:29:45.371612", - "last_modified_date": "2024-07-25 07:29:45.371612", - "version": 0, - "url": "https://ge.xhamster.com/videos/im-tasting-the-hot-juices-from-my-horny-mummys-cunt-stepmother-and-stepdaughter-xhyeR33", - "review": 0, - "should_download": 0, - "title": "Ich probiere die hei\u00dfen S\u00e4fte aus der Fotze meiner geilen Mami! Stiefmutter und Stieftochter! | xHamster", - "file_name": "Ich probiere die hei\u00dfen S\u00e4fte aus der Fotze meiner geilen Mami! Stiefmutter und Stieftochter! [xhyeR33].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/5f6e1fc0-2430-41b0-813a-a53ed494a464.mp4" - }, - { - "id": "5f8b2177-2862-4e38-a9e2-9a925657e46b", - "created_date": "2024-07-25 07:29:46.663107", - "last_modified_date": "2024-07-25 07:29:46.663107", - "version": 0, - "url": "https://ge.xhamster.com/videos/wanna-taste-your-stepbros-cum-lulu-chu-asks-maria-kazi-s25-e12-xhFkD8i", - "review": 0, - "should_download": 0, - "title": "\"willst du das sperma deines stiefbruers probieren?\" Lulu Chu fragt Maria Kazi- S25: E12 | xHamster", - "file_name": "\uff02willst du das sperma deines stiefbruers probieren\uff1f\uff02 Lulu Chu fragt Maria Kazi- S25\uff1a E12 [xhFkD8i].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/5f8b2177-2862-4e38-a9e2-9a925657e46b.mp4" - }, - { - "id": "6043593a-0e4c-4fb0-9b08-89c23d92fdf7", - "created_date": "2024-07-25 07:29:44.602877", - "last_modified_date": "2024-07-25 07:29:44.602877", - "version": 0, - "url": "https://ge.xhamster.com/videos/bratty-sis-slutty-sisters-fight-over-step-dad-s-cock-8469724", - "review": 0, - "should_download": 0, - "title": "Bratty Sis - versaute Schwestern k\u00e4mpfen um den Schwanz von Stiefvater | xHamster", - "file_name": "Bratty Sis - versaute Schwestern k\u00e4mpfen um den Schwanz von Stiefvater [8469724].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6043593a-0e4c-4fb0-9b08-89c23d92fdf7.mp4" - }, - { - "id": "606db219-6651-4e0a-8dc3-78f374f24b58", - "created_date": "2024-07-25 07:29:44.374238", - "last_modified_date": "2024-07-25 07:29:44.374238", - "version": 0, - "url": "https://ge.xhamster.com/videos/fam-63-xh2dim3", - "review": 0, - "should_download": 0, - "title": "Fam 63: Free Porn Video 6b | xHamster", - "file_name": "fam 63 [xh2dim3].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/606db219-6651-4e0a-8dc3-78f374f24b58.mp4" - }, - { - "id": "60896198-1e31-4730-b896-eeead1e206fb", - "created_date": "2024-07-25 07:29:48.118471", - "last_modified_date": "2024-07-25 07:29:48.118471", - "version": 0, - "url": "https://ge.xhamster.com/videos/creamed-stepsis-pussy-while-step-dad-sleeps-s9-e2-10936147", - "review": 0, - "should_download": 0, - "title": "Sahnige Stiefschwester-Muschi gesahnt, w\u00e4hrend Stiefvater s9: e2 schl\u00e4ft | xHamster", - "file_name": "Sahnige Stiefschwester-Muschi gesahnt, w\u00e4hrend Stiefvater s9\uff1a e2 schl\u00e4ft [10936147].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/60896198-1e31-4730-b896-eeead1e206fb.mp4" - }, - { - "id": "60ab814e-7b3e-4a15-94d6-5b61b3392969", - "created_date": "2024-07-25 07:29:48.017410", - "last_modified_date": "2024-07-25 07:29:48.017410", - "version": 0, - "url": "https://ge.xhamster.com/videos/18-and-confused-6-xhv0Cil", - "review": 0, - "should_download": 0, - "title": "18 und verwirrt # 6 | xHamster", - "file_name": "18 und verwirrt # 6 [xhv0Cil].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/60ab814e-7b3e-4a15-94d6-5b61b3392969.mp4" - }, - { - "id": "60c70b0e-82a6-4727-97a1-ad1c3db3b6f8", - "created_date": "2024-07-25 07:29:48.147842", - "last_modified_date": "2024-07-25 07:29:48.147842", - "version": 0, - "url": "https://ge.xhamster.com/videos/teeny-express-13221400", - "review": 0, - "should_download": 0, - "title": "Teeny-Express | xHamster", - "file_name": "Teeny-Express [13221400].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/60c70b0e-82a6-4727-97a1-ad1c3db3b6f8.mp4" - }, - { - "id": "60c8dea2-91b6-4bb3-8060-cc0ebbc3e015", - "created_date": "2024-07-25 07:29:46.045811", - "last_modified_date": "2024-07-25 07:29:46.045811", - "version": 0, - "url": "https://ge.xhamster.com/videos/panties-on-fire-1979-9357764", - "review": 0, - "should_download": 0, - "title": "H\u00f6schen in Flammen (1979) | xHamster", - "file_name": "H\u00f6schen in Flammen (1979) [9357764].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/60c8dea2-91b6-4bb3-8060-cc0ebbc3e015.mp4" - }, - { - "id": "60d91fe5-8e74-48d7-add7-7a67089a2a93", - "created_date": "2024-07-25 07:29:46.130104", - "last_modified_date": "2024-07-25 07:29:46.130104", - "version": 0, - "url": "https://ge.xhamster.com/videos/sex-orgy-830-10963835", - "review": 0, - "should_download": 0, - "title": "Sexorgie 830 | xHamster", - "file_name": "Sexorgie 830 [10963835].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/60d91fe5-8e74-48d7-add7-7a67089a2a93.mp4" - }, - { - "id": "6149f5a7-f33d-4c08-a922-d04fc88f4880", - "created_date": "2024-07-25 07:29:45.579826", - "last_modified_date": "2024-07-25 07:29:45.579826", - "version": 0, - "url": "https://ge.xhamster.com/videos/familie-matuschek-die-verfickte-hochzeit-avi-mp4-openload-8309689", - "review": 0, - "should_download": 0, - "title": "Familie Matuschek - Die Verfickte Hochzeit Avi Mp4 Openload | xHamster", - "file_name": "Familie Matuschek - Die verfickte Hochzeit.avi.mp4 openload. [8309689].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/6149f5a7-f33d-4c08-a922-d04fc88f4880.mp4" - }, - { - "id": "61a80611-e7f1-49b6-b222-60c7c54ba745", - "created_date": "2024-07-25 07:29:44.842853", - "last_modified_date": "2024-07-25 07:29:44.842853", - "version": 0, - "url": "https://ge.xhamster.com/videos/mutual-masturbation-with-neighbour-while-wife-is-watching-cumshot-programmerswife-xhKvype", - "review": 0, - "should_download": 0, - "title": "Gegenseitige masturbation mit dem nachbarn, w\u00e4hrend ehefrau zuschaut, abspritzen - ProgrammersWife | xHamster", - "file_name": "Gegenseitige masturbation mit dem nachbarn, w\u00e4hrend ehefrau zuschaut, abspritzen - ProgrammersWife [xhKvype].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/61a80611-e7f1-49b6-b222-60c7c54ba745.mp4" - }, - { - "id": "61b149ad-b830-4de1-be29-43ee3fa5f3c0", - "created_date": "2024-07-25 07:29:46.383126", - "last_modified_date": "2024-07-25 07:29:46.383126", - "version": 0, - "url": "https://ge.xhamster.com/videos/mother-and-daughter-decide-to-get-down-for-some-family-fun-7894256", - "review": 0, - "should_download": 0, - "title": "Mutter und Tochter beschlie\u00dfen, f\u00fcr etwas Familienspa\u00df runterzukommen | xHamster", - "file_name": "Mutter und Tochter beschlie\u00dfen, f\u00fcr etwas Familienspa\u00df runterzukommen [7894256].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/61b149ad-b830-4de1-be29-43ee3fa5f3c0.mp4" - }, - { - "id": "61b43c4f-4f94-4ba7-a7e5-87b9675e2196", - "created_date": "2024-12-29 23:53:27.883229", - "last_modified_date": "2025-01-03 00:49:09.828000", - "version": 3, - "url": "https://ge.xhamster.com/videos/vixen-petite-cute-young-intern-coco-seduces-her-older-boss-xh9Yz38", - "review": 0, - "should_download": 0, - "title": "Vixen - die zierliche, s\u00fc\u00dfe junge Praktikantin Coco verf\u00fchrt ihren \u00e4lteren Chef | xHamster", - "file_name": "Vixen - die zierliche, s\u00fc\u00dfe junge Praktikantin Coco verf\u00fchrt ihren \u00e4lteren Chef [xh9Yz38].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/61b43c4f-4f94-4ba7-a7e5-87b9675e2196.mp4" - }, - { - "id": "61b4b5d1-5284-4515-9fba-35878b0dcf9d", - "created_date": "2024-07-25 07:29:47.055261", - "last_modified_date": "2024-07-25 07:29:47.055261", - "version": 0, - "url": "https://ge.xhamster.com/videos/busty-teen-with-perfect-ass-blair-hudson-visits-her-free-use-step-aunt-step-uncle-familystrokes-xhgJ2os", - "review": 0, - "should_download": 0, - "title": "Vollbusiges teen mit perfektem arsch blair hudson besucht ihre kostenlose stieftante & stiefsohn-onkel, familystrokes | xHamster", - "file_name": "Vollbusiges teen mit perfektem arsch blair hudson besucht ihre kostenlose stieftante & stiefsohn-onkel, familystrokes [xhgJ2os].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/61b4b5d1-5284-4515-9fba-35878b0dcf9d.mp4" - }, - { - "id": "61c06fde-c3db-40cc-b82a-5072c75ad26a", - "created_date": "2024-12-29 23:53:27.924415", - "last_modified_date": "2024-12-29 23:53:27.924415", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-mischiefs-part-2-bf-wants-to-go-again-creampies-gf-while-her-step-moms-in-the-shower-xhvVr4i", - "review": 0, - "should_download": 0, - "title": "Familien-unfug (teil 2): freund will wieder gehen! Typ spritzt freundin voll, w\u00e4hrend ihre stiefmutter unter der dusche ist | xHamster", - "file_name": "Familien-unfug (teil 2)\uff1a freund will wieder gehen! Typ spritzt freundin voll, w\u00e4hrend ihre stiefmutter unter der dusche ist [xhvVr4i].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/61c06fde-c3db-40cc-b82a-5072c75ad26a.mp4" - }, - { - "id": "61ddca63-0dc5-4c7b-9250-e543d814085b", - "created_date": "2024-09-24 08:11:38.999581", - "last_modified_date": "2024-10-21 16:29:04.143000", - "version": 1, - "url": "https://ge.xhamster.com/videos/mommys-boy-alison-rey-learns-how-to-fuck-with-her-boyfriends-stepmoms-help-ffm-threesome-xh5HrfI", - "review": 0, - "should_download": 0, - "title": "Mamas Junge - Alison Rey lernt mit Hilfe der Stiefmutter ihres Freundes zu ficken! ffm Dreier! | xHamster", - "file_name": "Mamas Junge - Alison Rey lernt mit Hilfe der Stiefmutter ihres Freundes zu ficken! ffm Dreier! [xh5HrfI].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/61ddca63-0dc5-4c7b-9250-e543d814085b.mp4" - }, - { - "id": "622de32e-8bb0-4c76-a2fc-f7bb90914970", - "created_date": "2024-07-25 07:29:47.934225", - "last_modified_date": "2024-07-25 07:29:47.934225", - "version": 0, - "url": "https://ge.xhamster.com/videos/i-wasnt-planning-on-having-sex-with-my-step-son-xhZ5hg9", - "review": 0, - "should_download": 0, - "title": "Ich hatte nicht vor, Sex mit meinem Stiefsohn zu haben | xHamster", - "file_name": "Ich hatte nicht vor, Sex mit meinem Stiefsohn zu haben [xhZ5hg9].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/622de32e-8bb0-4c76-a2fc-f7bb90914970.mp4" - }, - { - "id": "629deea6-a275-4900-b439-fe7e946c2c72", - "created_date": "2024-08-09 20:22:02.076793", - "last_modified_date": "2024-08-16 10:30:54.797000", - "version": 1, - "url": "https://ge.xhamster.com/videos/my-hot-redhead-gf-shares-my-cock-with-her-bff-sonia-harcourt-jay-taylor-xh7x3cJ", - "review": 0, - "should_download": 0, - "title": "Meine hei\u00dfe rothaarige freundin teilt meinen schwanz mit ihrem besten freund - sonia harcourt & jay taylor | xHamster", - "file_name": "Meine hei\u00dfe rothaarige freundin teilt meinen schwanz mit ihrem besten freund - sonia harcourt & jay taylor [xh7x3cJ].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/629deea6-a275-4900-b439-fe7e946c2c72.mp4" - }, - { - "id": "62ad174e-ae76-495a-bf8d-5099e4dafa41", - "created_date": "2024-07-25 07:29:46.904180", - "last_modified_date": "2024-07-25 07:29:46.904180", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-swap-ready-set-kiss-with-sharon-white-bonnie-dolce-s7-e6-xhCsqcx", - "review": 0, - "should_download": 0, - "title": "Familientausch: bereit, gesetzt, KISS - mit sharon white & bonnie dolce- s7: e6 | xHamster", - "file_name": "Familientausch\uff1a bereit, gesetzt, KISS - mit sharon white & bonnie dolce- s7\uff1a e6 [xhCsqcx].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/62ad174e-ae76-495a-bf8d-5099e4dafa41.mp4" - }, - { - "id": "62cff9b0-fe6a-4301-b091-46e5951b6b77", - "created_date": "2024-07-25 07:29:45.595114", - "last_modified_date": "2024-07-25 07:29:45.595114", - "version": 0, - "url": "https://ge.xhamster.com/videos/daddy-cool-1999-xhthmkq", - "review": 0, - "should_download": 0, - "title": "Papi ist cool, 1999 | xHamster", - "file_name": "Papi ist cool, 1999 [xhthmkq].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/62cff9b0-fe6a-4301-b091-46e5951b6b77.mp4" - }, - { - "id": "62f854c5-2695-4b68-b32a-dd7ca6f93a01", - "created_date": "2024-07-25 07:29:47.194934", - "last_modified_date": "2024-07-25 07:29:47.194934", - "version": 0, - "url": "https://ge.xhamster.com/videos/das-haus-der-geheimen-luste-1979-xhwJoJs", - "review": 0, - "should_download": 0, - "title": "Das Haus Der Geheimen Luste 1979, Free Porn ae | xHamster", - "file_name": "Das Haus Der Geheimen Luste ( 1979 ) [xhwJoJs].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/62f854c5-2695-4b68-b32a-dd7ca6f93a01.mp4" - }, - { - "id": "633708a4-f248-4361-8f7c-97e6b29e5d90", - "created_date": "2024-07-25 07:29:45.456831", - "last_modified_date": "2024-07-25 07:29:45.456831", - "version": 0, - "url": "https://ge.xhamster.com/videos/innocent-teens-become-public-pussy-eating-sluts-14050190", - "review": 0, - "should_download": 0, - "title": "Unschuldige Teenager werden \u00f6ffentlich Muschi-essende Schlampen | xHamster", - "file_name": "Unschuldige Teenager werden \u00f6ffentlich Muschi-essende Schlampen [14050190].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/633708a4-f248-4361-8f7c-97e6b29e5d90.mp4" - }, - { - "id": "6362f8c6-2b1d-43e4-b246-a3289a509cf8", - "created_date": "2024-07-25 07:29:47.911108", - "last_modified_date": "2024-07-25 07:29:47.911108", - "version": 0, - "url": "https://ge.xhamster.com/videos/hot-step-aunt-thinks-shes-cool-and-lets-me-fuck-her-ass-jordan-maxx-xh4FVDA", - "review": 0, - "should_download": 0, - "title": "Hei\u00dfe Stieftante h\u00e4lt sie f\u00fcr cool und l\u00e4sst mich ihren Arsch ficken - Jordan Maxx | xHamster", - "file_name": "Hei\u00dfe Stieftante h\u00e4lt sie f\u00fcr cool und l\u00e4sst mich ihren Arsch ficken - Jordan Maxx [xh4FVDA].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6362f8c6-2b1d-43e4-b246-a3289a509cf8.mp4" - }, - { - "id": "63636ea1-a64f-41f3-a43f-fd4bbab0a3bf", - "created_date": "2024-07-25 07:29:44.628400", - "last_modified_date": "2024-07-25 07:29:44.628400", - "version": 0, - "url": "https://ge.xhamster.com/videos/german-classic-2964283", - "review": 0, - "should_download": 0, - "title": "Deutscher Klassiker | xHamster", - "file_name": "Deutscher Klassiker [2964283].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/63636ea1-a64f-41f3-a43f-fd4bbab0a3bf.mp4" - }, - { - "id": "6455f8dc-d334-4718-8293-e879e28a7e11", - "created_date": "2025-01-05 14:37:11.046000", - "last_modified_date": "2025-01-05 14:40:21.685000", - "version": 1, - "url": "https://ge.xhamster.com/videos/couple-caught-in-the-act-by-friends-11871326", - "review": 0, - "should_download": 0, - "title": "Paar von Freunden erwischt | xHamster", - "file_name": "Paar von Freunden erwischt [11871326].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/6455f8dc-d334-4718-8293-e879e28a7e11.mp4" - }, - { - "id": "649e9cb6-4252-4093-a9ae-36f13e74da62", - "created_date": "2024-07-25 07:29:47.322945", - "last_modified_date": "2024-07-25 07:29:47.322945", - "version": 0, - "url": "https://ge.xhamster.com/videos/die-traumfrau-xhXWPzp", - "review": 0, - "should_download": 0, - "title": "Die Traumfrau: Free European HD Porn Video 9a | xHamster", - "file_name": "Die Traumfrau [xhXWPzp].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/649e9cb6-4252-4093-a9ae-36f13e74da62.mp4" - }, - { - "id": "64e26069-eb61-432a-892b-83adbe6abacb", - "created_date": "2024-07-25 07:29:47.063358", - "last_modified_date": "2024-07-25 07:29:47.063358", - "version": 0, - "url": "https://ge.xhamster.com/videos/fa-cute-schoolgirl-gets-the-help-she-wanted-13904739", - "review": 0, - "should_download": 0, - "title": "Fa, s\u00fc\u00dfes Schulm\u00e4dchen bekommt die Hilfe, die sie wollte! | xHamster", - "file_name": "Fa, s\u00fc\u00dfes Schulm\u00e4dchen bekommt die Hilfe, die sie wollte! [13904739].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/64e26069-eb61-432a-892b-83adbe6abacb.mp4" - }, - { - "id": "64f0fc42-17f6-48d4-a1b6-a97ec197a25b", - "created_date": "2024-07-25 07:29:45.835813", - "last_modified_date": "2024-07-25 07:29:45.835813", - "version": 0, - "url": "https://ge.xhamster.com/videos/josefine-mutzenbacher-the-whore-of-vienna-xh7Gj8r", - "review": 0, - "should_download": 0, - "title": "Josefine Mutzenbacher Die Hure Von Wien | xHamster", - "file_name": "Josefine Mutzenbacher Die Hure Von Wien [xh7Gj8r].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/64f0fc42-17f6-48d4-a1b6-a97ec197a25b.mp4" - }, - { - "id": "6508e9a7-ca3b-4be7-b177-3ac17272facd", - "created_date": "2024-07-25 07:29:46.411730", - "last_modified_date": "2024-07-25 07:29:46.411730", - "version": 0, - "url": "https://ge.xhamster.com/videos/der-perfekte-angestellte-nimmt-es-in-den-arsch-full-movie-xhmyyMG", - "review": 0, - "should_download": 0, - "title": "Der Perfekte Angestellte Nimmt Es in Den Arsch Full Movie | xHamster", - "file_name": "Der perfekte Angestellte nimmt es in den Arsch (Full Movie) [xhmyyMG].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6508e9a7-ca3b-4be7-b177-3ac17272facd.mp4" - }, - { - "id": "654ce8a0-807e-4b68-8356-87903642bd5a", - "created_date": "2024-10-21 15:08:43.550005", - "last_modified_date": "2024-10-21 16:29:09.478000", - "version": 1, - "url": "https://ge.xhamster.com/videos/hot-secretary-gets-fucked-by-three-horny-co-workers-xhaBfNF", - "review": 0, - "should_download": 0, - "title": "Hei\u00dfe sekret\u00e4rin wird von drei geilen kollegen gefickt | xHamster", - "file_name": "Hei\u00dfe sekret\u00e4rin wird von drei geilen kollegen gefickt [xhaBfNF].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/654ce8a0-807e-4b68-8356-87903642bd5a.mp4" - }, - { - "id": "65cd77d4-b124-4dce-9098-25f16714cd32", - "created_date": "2024-07-25 07:29:46.715263", - "last_modified_date": "2024-07-25 07:29:46.715263", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-cousins-like-anal-creampies-full-hd-movie-original-xhTCHpH", - "review": 0, - "should_download": 0, - "title": "Meine Cousins m\u00f6gen anal Creampies - (Full HD-Film - Original) | xHamster", - "file_name": "Meine Cousins m\u00f6gen anal Creampies - (Full HD-Film - Original) [xhTCHpH].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/65cd77d4-b124-4dce-9098-25f16714cd32.mp4" - }, - { - "id": "65d66794-a62f-4cae-bc10-11c120f186df", - "created_date": "2024-07-25 07:29:47.805084", - "last_modified_date": "2024-07-25 07:29:47.805084", - "version": 0, - "url": "https://ge.xhamster.com/videos/tabooheat-theres-enough-cory-chase-for-all-her-sneaky-stepfamily-xhejEOr", - "review": 0, - "should_download": 0, - "title": "Tabooheat \u2013 Es gibt genug cory Chase f\u00fcr all ihre hinterh\u00e4ltige stieffamilie! | xHamster", - "file_name": "Tabooheat \u2013 Es gibt genug cory Chase f\u00fcr all ihre hinterh\u00e4ltige stieffamilie! [xhejEOr].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/65d66794-a62f-4cae-bc10-11c120f186df.mp4" - }, - { - "id": "6616a23b-436e-4b2f-82b6-9941e07f96f6", - "created_date": "2025-01-05 14:36:11.957000", - "last_modified_date": "2025-01-05 14:36:38.381000", - "version": 1, - "url": "https://ge.xhamster.com/videos/familystrokes-gorgeous-cutie-offers-all-her-holes-to-appease-stepbros-for-all-her-wrong-doings-xhr6HZ3", - "review": 0, - "should_download": 0, - "title": "Familystrokes - die wundersch\u00f6ne S\u00fc\u00dfe bietet alle ihre L\u00f6cher an, um Stiefbruder f\u00fcr all ihre falschen Taten zu bes\u00e4nftigen | xHamster", - "file_name": "Familystrokes - die wundersch\u00f6ne S\u00fc\u00dfe bietet alle ihre L\u00f6cher an, um Stiefbruder f\u00fcr all ihre falschen Taten zu bes\u00e4nftigen [xhr6HZ3].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/6616a23b-436e-4b2f-82b6-9941e07f96f6.mp4" - }, - { - "id": "663478bd-89ac-4a83-bc1d-87c3a34ec485", - "created_date": "2024-07-25 07:29:45.072108", - "last_modified_date": "2024-07-25 07:29:45.072108", - "version": 0, - "url": "https://ge.xhamster.com/videos/kinky-family-blair-williams-fucking-my-hot-big-ass-steps-8799232", - "review": 0, - "should_download": 0, - "title": "Versaute Familie - Blair Williams - fickt meine hei\u00dfen Schritte mit dickem Arsch | xHamster", - "file_name": "Versaute Familie - Blair Williams - fickt meine hei\u00dfen Schritte mit dickem Arsch [8799232].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/663478bd-89ac-4a83-bc1d-87c3a34ec485.mp4" - }, - { - "id": "663e820a-b8d1-4711-8dd8-78a1476e4d1d", - "created_date": "2024-07-25 07:29:44.462307", - "last_modified_date": "2024-07-25 07:29:44.462307", - "version": 0, - "url": "https://ge.xhamster.com/videos/uncontrollable-load-blown-in-stepdaughters-mouth-by-lucky-stepdad-xh8dYFa", - "review": 0, - "should_download": 0, - "title": "Unkontrollierbare ladung wird von gl\u00fccklichem stiefvater in den mund der stieftochter geblasen | xHamster", - "file_name": "Unkontrollierbare ladung wird von gl\u00fccklichem stiefvater in den mund der stieftochter geblasen [xh8dYFa].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/663e820a-b8d1-4711-8dd8-78a1476e4d1d.mp4" - }, - { - "id": "6648bb75-f56f-4f58-a3cb-0dd5746918f1", - "created_date": "2024-07-25 07:29:45.090874", - "last_modified_date": "2024-07-25 07:29:45.090874", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepmom-on-vacation-seduces-stepson-on-the-beach-pov-xhVLQCp", - "review": 0, - "should_download": 0, - "title": "Stiefmutter im Urlaub verf\u00fchrt Stiefsohn am Strand (POV) | xHamster", - "file_name": "Stiefmutter im Urlaub verf\u00fchrt Stiefsohn am Strand (POV) [xhVLQCp].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6648bb75-f56f-4f58-a3cb-0dd5746918f1.mp4" - }, - { - "id": "6678b33d-eda5-49fb-a2a1-c9ac9ab034fb", - "created_date": "2024-07-25 07:29:47.213746", - "last_modified_date": "2024-07-25 07:29:47.213746", - "version": 0, - "url": "https://ge.xhamster.com/videos/sex-office-part-6-four-big-dicks-for-employee-of-the-month-xhdfCVT", - "review": 0, - "should_download": 0, - "title": "Sexb\u00fcro, teil 6. Vier gro\u00dfe schw\u00e4nze f\u00fcr angestellte des monats | xHamster", - "file_name": "Sexb\u00fcro, teil 6. Vier gro\u00dfe schw\u00e4nze f\u00fcr angestellte des monats [xhdfCVT].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6678b33d-eda5-49fb-a2a1-c9ac9ab034fb.mp4" - }, - { - "id": "66ab05fa-543c-4097-8c10-eb55eefd952f", - "created_date": "2024-07-25 07:29:46.587412", - "last_modified_date": "2024-07-25 07:29:46.587412", - "version": 0, - "url": "https://ge.xhamster.com/videos/you-earned-it-8148174", - "review": 0, - "should_download": 0, - "title": "Du hast es verdient | xHamster", - "file_name": "Du hast es verdient [8148174].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/66ab05fa-543c-4097-8c10-eb55eefd952f.mp4" - }, - { - "id": "66b0aaf0-1df1-4831-ae68-e851f8c57157", - "created_date": "2024-07-25 07:29:46.971179", - "last_modified_date": "2024-07-25 07:29:46.971179", - "version": 0, - "url": "https://ge.xhamster.com/videos/der-saft-muss-raus-1976-10203335", - "review": 0, - "should_download": 0, - "title": "Der Saft Muss Raus 1976, Free European HD Porn ed | xHamster", - "file_name": "Der Saft muss raus ... (1976) [10203335].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/66b0aaf0-1df1-4831-ae68-e851f8c57157.mp4" - }, - { - "id": "673fd24c-4bb1-4cf4-a5cf-baaa60185832", - "created_date": "2024-08-28 23:21:54.345181", - "last_modified_date": "2024-08-28 23:21:54.345181", - "version": 0, - "url": "https://ge.xhamster.com/videos/a-filthy-family-bi-video-10819568", - "review": 0, - "should_download": 0, - "title": "Ein schmutziges Familien-Bi-Video | xHamster", - "file_name": "Ein schmutziges Familien-Bi-Video [10819568].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/673fd24c-4bb1-4cf4-a5cf-baaa60185832.mp4" - }, - { - "id": "67629fc7-8a43-4681-afa3-0857b4db93b6", - "created_date": "2024-07-25 07:29:45.591444", - "last_modified_date": "2024-07-25 07:29:45.591444", - "version": 0, - "url": "https://ge.xhamster.com/videos/tight-teen-gets-her-fist-gangbang-an-stepdads-party-and-her-holes-are-not-virgin-anymore-xhaglVF", - "review": 0, - "should_download": 0, - "title": "Enge Teenie bekommt ihren ersten Gangbang auf der Party ihres Stiefvaters und ihre L\u00f6cher sind nicht mehr jungfr\u00e4ulich! | xHamster", - "file_name": "Enge Teenie bekommt ihren ersten Gangbang auf der Party ihres Stiefvaters und ihre L\u00f6cher sind nicht mehr jungfr\u00e4ulich! [xhaglVF].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/67629fc7-8a43-4681-afa3-0857b4db93b6.mp4" - }, - { - "id": "67bbefe9-272e-4770-b75d-08b76374c203", - "created_date": "2024-07-25 07:29:47.577760", - "last_modified_date": "2024-07-25 07:29:47.577760", - "version": 0, - "url": "https://ge.xhamster.com/videos/sexplosion-ibiza-1988-6208646", - "review": 0, - "should_download": 0, - "title": "Sexplosion Ibiza (1988) | xHamster", - "file_name": "Sexplosion Ibiza (1988) [6208646].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/67bbefe9-272e-4770-b75d-08b76374c203.mp4" - }, - { - "id": "67d59112-e5a3-468e-87eb-1dcd1add6268", - "created_date": "2024-07-25 07:29:47.468563", - "last_modified_date": "2024-07-25 07:29:47.468563", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepmom-if-you-want-pleasure-sit-on-my-dick-xhSB21d", - "review": 0, - "should_download": 0, - "title": "Stiefmutter: Wenn du Lust haben willst, setz dich auf meinen Schwanz | xHamster", - "file_name": "Stiefmutter\uff1a Wenn du Lust haben willst, setz dich auf meinen Schwanz [xhSB21d].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/67d59112-e5a3-468e-87eb-1dcd1add6268.mp4" - }, - { - "id": "686a5095-b3a8-43be-a9e1-390f4d0db810", - "created_date": "2024-07-25 07:29:47.442492", - "last_modified_date": "2024-07-25 07:29:47.442492", - "version": 0, - "url": "https://ge.xhamster.com/videos/i-made-up-my-mind-im-gonna-fuck-your-uncle-xhzbcRn", - "review": 0, - "should_download": 0, - "title": "Ich habe mich entschieden, ich werde deinen Onkel ficken! | xHamster", - "file_name": "Ich habe mich entschieden, ich werde deinen Onkel ficken! [xhzbcRn].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/686a5095-b3a8-43be-a9e1-390f4d0db810.mp4" - }, - { - "id": "6888c0ae-638d-4dbd-a49a-eea3bc844914", - "created_date": "2024-07-25 07:29:46.810116", - "last_modified_date": "2024-07-25 07:29:46.810116", - "version": 0, - "url": "https://ge.xhamster.com/videos/private-jessica-moore-enjoys-a-hot-dp-while-on-a-tropical-beach-xhm2biL", - "review": 0, - "should_download": 0, - "title": "Jessica Moore genie\u00dft eine hei\u00dfe Doppelpenetration an einem tropischen Strand | xHamster", - "file_name": "Jessica Moore genie\u00dft eine hei\u00dfe Doppelpenetration an einem tropischen Strand [xhm2biL].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6888c0ae-638d-4dbd-a49a-eea3bc844914.mp4" - }, - { - "id": "6902b6f8-cc57-457b-a704-ac68ef79c75c", - "created_date": "2024-07-25 07:29:47.650192", - "last_modified_date": "2024-07-25 07:29:47.650192", - "version": 0, - "url": "https://ge.xhamster.com/videos/momsteachsex-perv-milf-has-teen-please-sons-cock-s8-e7-10114594", - "review": 0, - "should_download": 0, - "title": "Momsteachsex - perverse MILF hat Teen bitte den Schwanz ihres Sohnes s8: e7 | xHamster", - "file_name": "Momsteachsex - perverse MILF hat Teen bitte den Schwanz ihres Sohnes s8\uff1a e7 [10114594].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6902b6f8-cc57-457b-a704-ac68ef79c75c.mp4" - }, - { - "id": "6915e5bb-6942-4cb8-9934-9af2a2b8085b", - "created_date": "2024-07-25 07:29:45.359614", - "last_modified_date": "2024-07-25 07:29:45.359614", - "version": 0, - "url": "https://ge.xhamster.com/videos/doppelter-genussclub-full-movie-xh9ag5T", - "review": 0, - "should_download": 0, - "title": "Doppelter Genussclub Full Movie, Free Porn be | xHamster", - "file_name": "Doppelter Genussclub (Full Movie) [xh9ag5T].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/6915e5bb-6942-4cb8-9934-9af2a2b8085b.mp4" - }, - { - "id": "69323afc-d33e-435a-b77b-118486b059cb", - "created_date": "2024-07-25 07:29:46.687851", - "last_modified_date": "2024-07-25 07:29:46.687851", - "version": 0, - "url": "https://ge.xhamster.com/videos/sex-club-holidays-1992-carol-lynn-beatrice-valle-kerry-s-9080782", - "review": 0, - "should_download": 0, - "title": "Sexclub-Ferien (1992) Carol Lynn, Beatrice Valle, Kerry s | xHamster", - "file_name": "Sexclub-Ferien (1992) Carol Lynn, Beatrice Valle, Kerry s [9080782].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/69323afc-d33e-435a-b77b-118486b059cb.mp4" - }, - { - "id": "699d58a2-51eb-4cad-af1f-c91721e652cb", - "created_date": "2024-07-25 07:29:45.411998", - "last_modified_date": "2024-07-25 07:29:45.411998", - "version": 0, - "url": "https://ge.xhamster.com/videos/porno-classics-vol-5-xhMdZ0E", - "review": 0, - "should_download": 0, - "title": "Die verfickten Zwillinge | xHamster", - "file_name": "Die verfickten Zwillinge [xhMdZ0E].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/699d58a2-51eb-4cad-af1f-c91721e652cb.mp4" - }, - { - "id": "6a295aa4-389d-4613-944b-dea59349759a", - "created_date": "2024-07-25 07:29:45.989649", - "last_modified_date": "2024-07-25 07:29:45.989649", - "version": 0, - "url": "https://ge.xhamster.com/videos/digitalplayground-my-best-friends-parents-carolina-sweets-9599204", - "review": 0, - "should_download": 0, - "title": "Digitalplayground - die Eltern meines besten Freundes Carolina Sweets | xHamster", - "file_name": "Digitalplayground - die Eltern meines besten Freundes Carolina Sweets [9599204].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6a295aa4-389d-4613-944b-dea59349759a.mp4" - }, - { - "id": "6a7f1452-ce56-4a36-b13a-2100f6e23d98", - "created_date": "2024-07-25 07:29:46.979361", - "last_modified_date": "2024-07-25 07:29:46.979361", - "version": 0, - "url": "https://ge.xhamster.com/videos/daddy4k-amazing-sex-action-of-older-stepdad-and-two-young-12698288", - "review": 0, - "should_download": 0, - "title": "Daddy4k. Erstaunliche Sex-Action von \u00e4lterem Stiefvater und zwei Jungen | xHamster", - "file_name": "Daddy4k. Erstaunliche Sex-Action von \u00e4lterem Stiefvater und zwei Jungen [12698288].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6a7f1452-ce56-4a36-b13a-2100f6e23d98.mp4" - }, - { - "id": "6b02a638-3caf-4e79-85b9-375c350abf43", - "created_date": "2024-07-25 07:29:46.019161", - "last_modified_date": "2024-07-25 07:29:46.019161", - "version": 0, - "url": "https://ge.xhamster.com/videos/happy-stepmom-gets-a-double-birthday-special-from-her-loving-step-son-his-huge-dick-friend-mylf-xhcRjUB", - "review": 0, - "should_download": 0, - "title": "Gl\u00fcckliche stiefmutter bekommt ein doppeltes geburtstags-special von ihrem liebenden stiefsohn und seiner freundin mit riesigem schwanz - MYLF | xHamster", - "file_name": "Gl\u00fcckliche stiefmutter bekommt ein doppeltes geburtstags-special von ihrem liebenden stiefsohn und seiner freundin mit riesigem schwanz - MYLF [xhcRjUB].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6b02a638-3caf-4e79-85b9-375c350abf43.mp4" - }, - { - "id": "6b1961e2-2038-421b-a6d2-43bc20226b20", - "created_date": "2024-07-25 07:29:48.063150", - "last_modified_date": "2024-07-25 07:29:48.063150", - "version": 0, - "url": "https://ge.xhamster.com/videos/bumsfidele-fick-hotel-das-bums-fidele-fick-hotel-xhbu67u", - "review": 0, - "should_download": 0, - "title": "Bumsfidele Fick-hotel - Das Bums-fidele Fick-hotel: Porn a8 | xHamster", - "file_name": "Bumsfidele Fick-Hotel - Das bums-fidele Fick-Hotel [xhbu67u].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6b1961e2-2038-421b-a6d2-43bc20226b20.mp4" - }, - { - "id": "6b5b8586-e116-455a-90f7-3dfb82715701", - "created_date": "2024-07-25 07:29:44.311214", - "last_modified_date": "2024-07-25 07:29:44.311214", - "version": 0, - "url": "https://ge.xhamster.com/videos/geschwisterliebe-ist-strafbar-9149693", - "review": 0, - "should_download": 0, - "title": "Geschwisterliebe Ist Strafbar, Free German Porn Video a8 | xHamster", - "file_name": "Geschwisterliebe ist Strafbar [9149693].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6b5b8586-e116-455a-90f7-3dfb82715701.mp4" - }, - { - "id": "6b9fc3ed-ee15-4902-9304-e9963234ff41", - "created_date": "2024-07-25 07:29:47.340860", - "last_modified_date": "2024-07-25 07:29:47.340860", - "version": 0, - "url": "https://ge.xhamster.com/videos/outdoor-group-sex-with-horny-vacationers-xhOcml6", - "review": 0, - "should_download": 0, - "title": "Outdoor Gruppensex mit geilen Urlauberinnen | xHamster", - "file_name": "Outdoor Gruppensex mit geilen Urlauberinnen [xhOcml6].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6b9fc3ed-ee15-4902-9304-e9963234ff41.mp4" - }, - { - "id": "6bb28414-2353-4fc4-970f-904236dd4344", - "created_date": "2024-07-25 07:29:44.749467", - "last_modified_date": "2024-07-25 07:29:44.749467", - "version": 0, - "url": "https://ge.xhamster.com/videos/suburban-taboo-xhZqZDG", - "review": 0, - "should_download": 0, - "title": "Vorstadt-Tabu | xHamster", - "file_name": "Vorstadt-Tabu [xhZqZDG].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6bb28414-2353-4fc4-970f-904236dd4344.mp4" - }, - { - "id": "6bd69b1f-c08e-46ab-be7e-f157b9322e2b", - "created_date": "2024-07-25 07:29:47.383071", - "last_modified_date": "2024-07-25 07:29:47.383071", - "version": 0, - "url": "https://ge.xhamster.com/videos/reifeprufung-inder-sex-schule-full-movie-1157605", - "review": 0, - "should_download": 0, - "title": "Reifeprufung inder Sex-Schule, kompletter Film | xHamster", - "file_name": "Reifeprufung inder Sex-Schule, kompletter Film [1157605].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6bd69b1f-c08e-46ab-be7e-f157b9322e2b.mp4" - }, - { - "id": "6be6d612-883f-4ee0-b330-f9b8312e89da", - "created_date": "2024-07-25 07:29:47.156065", - "last_modified_date": "2024-07-25 07:29:47.156065", - "version": 0, - "url": "https://ge.xhamster.com/videos/all-in-the-family-1972-10203342", - "review": 0, - "should_download": 0, - "title": "Alles in der Familie (1972) | xHamster", - "file_name": "Alles in der Familie (1972) [10203342].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6be6d612-883f-4ee0-b330-f9b8312e89da.mp4" - }, - { - "id": "6c44c384-0b6e-4d82-a26e-3d405a39c669", - "created_date": "2024-07-25 07:29:44.543623", - "last_modified_date": "2024-07-25 07:29:44.543623", - "version": 0, - "url": "https://ge.xhamster.com/videos/saggy-tits-secretary-fucked-by-her-boss-and-three-other-employees-xh9yGkT", - "review": 0, - "should_download": 0, - "title": "H\u00e4ngende Sekret\u00e4rin von ihrem Chef und drei anderen Angestellten gefickt | xHamster", - "file_name": "H\u00e4ngende Sekret\u00e4rin von ihrem Chef und drei anderen Angestellten gefickt [xh9yGkT].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6c44c384-0b6e-4d82-a26e-3d405a39c669.mp4" - }, - { - "id": "6c7c3cd7-f1ac-4844-a840-b7747911ea63", - "created_date": "2024-09-11 10:23:29.175970", - "last_modified_date": "2024-10-21 16:29:16.277000", - "version": 1, - "url": "https://ge.xhamster.com/videos/shocked-stepmom-catches-her-stepson-ploughing-a-tight-pink-18-yrs-old-pussy-familystrokes-xhXUCEy", - "review": 0, - "should_download": 0, - "title": "Schockierte stiefmutter erwischt ihren stiefsohn beim pfl\u00fcgt eine enge rosa 18-j\u00e4hrige muschi, familienschl\u00e4ge | xHamster", - "file_name": "Schockierte stiefmutter erwischt ihren stiefsohn beim pfl\u00fcgt eine enge rosa 18-j\u00e4hrige muschi, familienschl\u00e4ge [xhXUCEy].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/6c7c3cd7-f1ac-4844-a840-b7747911ea63.mp4" - }, - { - "id": "6cb4856e-7fae-4cc0-a658-b3deee358a7c", - "created_date": "2024-07-25 07:29:47.020367", - "last_modified_date": "2024-07-25 07:29:47.020367", - "version": 0, - "url": "https://ge.xhamster.com/videos/redhead-and-brunette-teen-whores-have-hardcore-threesome-fuck-with-a-stud-xhwlFfU", - "review": 0, - "should_download": 0, - "title": "Rothaarige und br\u00fcnette Teen-Huren haben Hardcore-Dreier-Fick mit einem Hengst | xHamster", - "file_name": "Rothaarige und br\u00fcnette Teen-Huren haben Hardcore-Dreier-Fick mit einem Hengst [xhwlFfU].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6cb4856e-7fae-4cc0-a658-b3deee358a7c.mp4" - }, - { - "id": "6ccd5934-aa00-4e97-a2c6-9bbdcda970af", - "created_date": "2024-07-25 07:29:45.146437", - "last_modified_date": "2024-07-25 07:29:45.146437", - "version": 0, - "url": "https://ge.xhamster.com/videos/fuck-the-boss-to-save-my-job-shannon-heels-xhHfead", - "review": 0, - "should_download": 0, - "title": "Fick den Boss, um meinen Job zu retten - Shannon Heels | xHamster", - "file_name": "Fick den Boss, um meinen Job zu retten - Shannon Heels [xhHfead].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6ccd5934-aa00-4e97-a2c6-9bbdcda970af.mp4" - }, - { - "id": "6cf56d99-bf23-4ff7-abf1-07a09b6d5970", - "created_date": "2024-07-25 07:29:48.089654", - "last_modified_date": "2024-07-25 07:29:48.089654", - "version": 0, - "url": "https://ge.xhamster.com/videos/three-horny-amateur-guys-doing-one-cute-teen-girl-2368857", - "review": 0, - "should_download": 0, - "title": "Drei geile Amateur-Typen machen ein s\u00fc\u00dfes Teenie-M\u00e4dchen | xHamster", - "file_name": "Drei geile Amateur-Typen machen ein s\u00fc\u00dfes Teenie-M\u00e4dchen [2368857].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6cf56d99-bf23-4ff7-abf1-07a09b6d5970.mp4" - }, - { - "id": "6d52fb0e-eb74-4449-a8cb-162182c5ef43", - "created_date": "2024-10-21 15:08:43.553201", - "last_modified_date": "2024-10-21 16:29:21.662000", - "version": 1, - "url": "https://ge.xhamster.com/videos/sexy-dayana-is-waiting-for-two-guys-8918095", - "review": 0, - "should_download": 0, - "title": "Sexy Dayana wartet auf zwei Typen | xHamster", - "file_name": "Sexy Dayana wartet auf zwei Typen [8918095].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/6d52fb0e-eb74-4449-a8cb-162182c5ef43.mp4" - }, - { - "id": "6e13a93d-bc45-41ed-83d9-48e0042a1bcb", - "created_date": "2024-07-25 07:29:47.473394", - "last_modified_date": "2024-07-25 07:29:47.473394", - "version": 0, - "url": "https://ge.xhamster.com/videos/german-sexy-tales-full-movie-xhuWl0H", - "review": 0, - "should_download": 0, - "title": "Deutsche sexy Geschichten (kompletter Film) | xHamster", - "file_name": "Deutsche sexy Geschichten (kompletter Film) [xhuWl0H].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6e13a93d-bc45-41ed-83d9-48e0042a1bcb.mp4" - }, - { - "id": "6e40fd97-a6e3-41d2-a764-0ce71ebbba0d", - "created_date": "2024-07-25 07:29:47.982661", - "last_modified_date": "2024-07-25 07:29:47.982661", - "version": 0, - "url": "https://ge.xhamster.com/videos/hot-girl-next-door-fucks-her-stepdads-big-dick-while-stepmoms-away-xhX1aiY", - "review": 0, - "should_download": 0, - "title": "Hei\u00dfes m\u00e4dchen von nebenan fickt den gro\u00dfen schwanz ihres stiefvaters, w\u00e4hrend stiefmutter weg ist | xHamster", - "file_name": "Hei\u00dfes m\u00e4dchen von nebenan fickt den gro\u00dfen schwanz ihres stiefvaters, w\u00e4hrend stiefmutter weg ist [xhX1aiY].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6e40fd97-a6e3-41d2-a764-0ce71ebbba0d.mp4" - }, - { - "id": "6e4fe5c8-7c96-4ec7-98c5-1e600372a583", - "created_date": "2024-09-24 08:11:39.004291", - "last_modified_date": "2024-10-21 16:29:29.122000", - "version": 1, - "url": "https://ge.xhamster.com/videos/can-you-please-lick-my-pussy-begs-sweet-sophia-s26-e5-xhplI2PS", - "review": 0, - "should_download": 0, - "title": "\"Kannst du bitte meine Muschi lecken ??\" bittet, s\u00fc\u00dfe Sophia - s26: e5 | xHamster", - "file_name": ""Kannst du bitte meine Muschi lecken \uff1f\uff1f" bittet, s\u00fc\u00dfe Sophia - s26\uff1a e5 [xhplI2PS].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/6e4fe5c8-7c96-4ec7-98c5-1e600372a583.mp4" - }, - { - "id": "6e5ad66c-db2f-4431-a8ab-97df389d4fdd", - "created_date": "2024-07-25 07:29:46.095200", - "last_modified_date": "2024-07-25 07:29:46.095200", - "version": 0, - "url": "https://ge.xhamster.com/videos/summer-fever-sex-full-movie-xhxtYij", - "review": 0, - "should_download": 0, - "title": "Sommerfieber-Sex (kompletter Film) | xHamster", - "file_name": "Sommerfieber-Sex (kompletter Film) [xhxtYij].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6e5ad66c-db2f-4431-a8ab-97df389d4fdd.mp4" - }, - { - "id": "6e81505b-72d6-4825-bc3a-6442cc98c4c9", - "created_date": "2024-07-25 07:29:45.598839", - "last_modified_date": "2024-07-25 07:29:45.598839", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-full-hd-movie-original-uncut-version-xha44sY", - "review": 0, - "should_download": 0, - "title": "Familie - (Full HD-Film - urspr\u00fcngliche ungeschnittene Version) | xHamster", - "file_name": "Familie - (Full HD-Film - urspr\u00fcngliche ungeschnittene Version) [xha44sY].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/6e81505b-72d6-4825-bc3a-6442cc98c4c9.mp4" - }, - { - "id": "6f2f8d99-7805-4062-83ff-12b008fb9355", - "created_date": "2024-11-10 16:53:33.479608", - "last_modified_date": "2024-11-10 16:53:33.479608", - "version": 0, - "url": "https://ge.xhamster.com/videos/french-11521540", - "review": 0, - "should_download": 0, - "title": "Franz\u00f6sisch | xHamster", - "file_name": "Franz\u00f6sisch [11521540].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/6f2f8d99-7805-4062-83ff-12b008fb9355.mp4" - }, - { - "id": "6f3157a0-85ae-490d-8a3c-444e13d1af02", - "created_date": "2024-07-25 07:29:45.047314", - "last_modified_date": "2024-07-25 07:29:45.047314", - "version": 0, - "url": "https://ge.xhamster.com/videos/meine-schwester-das-geile-flittchen-akt1-by-mdj-cook-401586", - "review": 0, - "should_download": 0, - "title": "Meine schwester, das geile flittchen (akt1) von mdj.cook | xHamster", - "file_name": "Meine schwester, das geile flittchen (akt1) von mdj.cook [401586].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6f3157a0-85ae-490d-8a3c-444e13d1af02.mp4" - }, - { - "id": "6f77c412-1e17-44fc-a710-4c27d43d7bea", - "created_date": "2024-07-25 07:29:47.231793", - "last_modified_date": "2024-07-25 07:29:47.231793", - "version": 0, - "url": "https://ge.xhamster.com/videos/gf-says-i-can-still-taste-her-pussy-on-your-dick-xhmRTFp", - "review": 0, - "should_download": 0, - "title": "Freundin sagt: \"Ich kann ihre Muschi immer noch an deinem Schwanz schmecken\" | xHamster", - "file_name": "Freundin sagt\uff1a \uff02Ich kann ihre Muschi immer noch an deinem Schwanz schmecken\uff02 [xhmRTFp].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6f77c412-1e17-44fc-a710-4c27d43d7bea.mp4" - }, - { - "id": "6fbaa365-9263-4054-8ef6-6cc92232b1cd", - "created_date": "2024-07-25 07:29:47.922469", - "last_modified_date": "2024-07-25 07:29:47.922469", - "version": 0, - "url": "https://ge.xhamster.com/videos/creampie-she-gets-fucked-by-two-men-on-the-beach-3256398", - "review": 0, - "should_download": 0, - "title": "Creampie - sie wird von zwei M\u00e4nnern am Strand gefickt | xHamster", - "file_name": "Creampie - sie wird von zwei M\u00e4nnern am Strand gefickt [3256398].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6fbaa365-9263-4054-8ef6-6cc92232b1cd.mp4" - }, - { - "id": "6fc6c764-6802-4378-946e-6d01222f79e3", - "created_date": "2024-07-25 07:29:47.711521", - "last_modified_date": "2024-07-25 07:29:47.711521", - "version": 0, - "url": "https://ge.xhamster.com/videos/omg-german-family-fucks-with-brothers-stepdaughter-dirty-threesome-xhqAn06", - "review": 0, - "should_download": 0, - "title": "omg deutsche familie fickt mit Stieftochter des Bruders versauter dreckiger Dreier | xHamster", - "file_name": "omg deutsche familie fickt mit Stieftochter des Bruders versauter dreckiger Dreier [xhqAn06].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6fc6c764-6802-4378-946e-6d01222f79e3.mp4" - }, - { - "id": "6fcdfd01-fb65-4b45-ad9e-d12cc96f212a", - "created_date": "2024-07-25 07:29:47.330129", - "last_modified_date": "2024-07-25 07:29:47.330129", - "version": 0, - "url": "https://ge.xhamster.com/videos/step-nephew-fucks-his-stepaunt-again-family-fantacy-xhjZUAd", - "review": 0, - "should_download": 0, - "title": "Stiefneffe fickt wieder seine stieftante - familienfantasie | xHamster", - "file_name": "Stiefneffe fickt wieder seine stieftante - familienfantasie [xhjZUAd].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6fcdfd01-fb65-4b45-ad9e-d12cc96f212a.mp4" - }, - { - "id": "6fdf94e9-72f0-4b6c-b853-e57de60395e7", - "created_date": "2024-07-25 07:29:47.160008", - "last_modified_date": "2024-07-25 07:29:47.160008", - "version": 0, - "url": "https://ge.xhamster.com/videos/first-orgy-for-his-wife-xhkYZNe", - "review": 0, - "should_download": 0, - "title": "Erste Orgie f\u00fcr seine Ehefrau | xHamster", - "file_name": "Erste Orgie f\u00fcr seine Ehefrau [xhkYZNe].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/6fdf94e9-72f0-4b6c-b853-e57de60395e7.mp4" - }, - { - "id": "70134685-8910-40f5-b83c-84a2f7973fcd", - "created_date": "2024-08-28 23:21:54.378179", - "last_modified_date": "2024-08-28 23:21:54.378179", - "version": 0, - "url": "https://ge.xhamster.com/videos/a-steamy-afternoon-s18-e12-xhPvwgB", - "review": 0, - "should_download": 0, - "title": "Ein dampfiger nachmittag - s18: E12 | xHamster", - "file_name": "Ein dampfiger nachmittag - s18\uff1a E12 [xhPvwgB].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/70134685-8910-40f5-b83c-84a2f7973fcd.mp4" - }, - { - "id": "70666a85-f470-4b2b-bd5a-6bcb9ba2e7c2", - "created_date": "2024-07-25 07:29:46.049429", - "last_modified_date": "2024-07-25 07:29:46.049429", - "version": 0, - "url": "https://ge.xhamster.com/videos/swap-step-mom-says-im-naked-you-should-get-naked-too-xhfaim2", - "review": 0, - "should_download": 0, - "title": "Swap-Stiefmutter sagt, ich bin nackt, du solltest dich auch nackt machen! | xHamster", - "file_name": "Swap-Stiefmutter sagt, ich bin nackt, du solltest dich auch nackt machen! [xhfaim2].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/70666a85-f470-4b2b-bd5a-6bcb9ba2e7c2.mp4" - }, - { - "id": "708556d1-498b-4ccd-8ebc-9e78c06e9239", - "created_date": "2024-07-25 07:29:46.694986", - "last_modified_date": "2024-07-25 07:29:46.694986", - "version": 0, - "url": "https://ge.xhamster.com/videos/wilde-spiele-full-german-movie-xh18KdA", - "review": 0, - "should_download": 0, - "title": "Wilde Spiele- Full German Movie, Free Porn b3 | xHamster", - "file_name": "Wilde Spiele- full german movie [xh18KdA].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/708556d1-498b-4ccd-8ebc-9e78c06e9239.mp4" - }, - { - "id": "70860f46-ad0a-4ec9-b588-5e7059ff208c", - "created_date": "2024-07-25 07:29:47.348035", - "last_modified_date": "2024-07-25 07:29:47.348035", - "version": 0, - "url": "https://ge.xhamster.com/videos/huge-tits-secretary-caught-monster-cock-boss-fuck-and-join-14923480", - "review": 0, - "should_download": 0, - "title": "Mega Titten Sekret\u00e4rin erwischt Boss beim ficken und macht einfach mit | xHamster", - "file_name": "Mega Titten Sekret\u00e4rin erwischt Boss beim ficken und macht einfach mit [14923480].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/70860f46-ad0a-4ec9-b588-5e7059ff208c.mp4" - }, - { - "id": "709ef7de-4f5c-4e0c-82e7-be1c2e459e5d", - "created_date": "2024-07-25 07:29:47.639533", - "last_modified_date": "2024-07-25 07:29:47.639533", - "version": 0, - "url": "https://ge.xhamster.com/videos/dirty-diary-full-movie-xh4vFyh", - "review": 0, - "should_download": 0, - "title": "Schmutziges Tagebuch (kompletter Film) | xHamster", - "file_name": "Schmutziges Tagebuch (kompletter Film) [xh4vFyh].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/709ef7de-4f5c-4e0c-82e7-be1c2e459e5d.mp4" - }, - { - "id": "70aeb534-f28b-4ebc-85e5-13695d794dff", - "created_date": "2024-08-28 23:21:54.361625", - "last_modified_date": "2024-08-28 23:21:54.361625", - "version": 0, - "url": "https://ge.xhamster.com/videos/mofos-house-party-turns-into-orgy-4036263", - "review": 0, - "should_download": 0, - "title": "Mofos - Hausparty wird zur Orgie | xHamster", - "file_name": "Mofos - Hausparty wird zur Orgie [4036263].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/70aeb534-f28b-4ebc-85e5-13695d794dff.mp4" - }, - { - "id": "70b1c610-9a34-466c-88c7-43c97533183a", - "created_date": "2024-07-25 07:29:46.252903", - "last_modified_date": "2024-07-25 07:29:46.252903", - "version": 0, - "url": "https://ge.xhamster.com/videos/blond-secretary-fucking-with-boss-to-keep-her-job-xhw6hoD", - "review": 0, - "should_download": 0, - "title": "Blonde Sekret\u00e4rin fickt mit Chef, um ihren Job zu behalten | xHamster", - "file_name": "Blonde Sekret\u00e4rin fickt mit Chef, um ihren Job zu behalten [xhw6hoD].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/70b1c610-9a34-466c-88c7-43c97533183a.mp4" - }, - { - "id": "70b88562-f3d7-4666-b1bb-43556b6988eb", - "created_date": "2024-07-25 07:29:45.514489", - "last_modified_date": "2024-07-25 07:29:45.514489", - "version": 0, - "url": "https://ge.xhamster.com/videos/caligola-1979-xh0MCXj", - "review": 0, - "should_download": 0, - "title": "Caligola (1979) | xHamster", - "file_name": "Caligola (1979) [xh0MCXj].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/70b88562-f3d7-4666-b1bb-43556b6988eb.mp4" - }, - { - "id": "70fd70de-ff9f-400c-9d04-30e24255eb81", - "created_date": "2024-07-25 07:29:45.895911", - "last_modified_date": "2024-07-25 07:29:45.895911", - "version": 0, - "url": "https://ge.xhamster.com/videos/hemmungsloser-sommerfick-ganzer-film-xhfgd1t", - "review": 0, - "should_download": 0, - "title": "Hemmungsloser Sommerfick Ganzer Film, HD Porn 37 | xHamster", - "file_name": "Hemmungsloser Sommerfick (GANZER FILM) [xhfgd1t].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/70fd70de-ff9f-400c-9d04-30e24255eb81.mp4" - }, - { - "id": "7128f33b-c370-4a30-a3fd-d68e6d925d52", - "created_date": "2025-01-16 19:59:30.841299", - "last_modified_date": "2025-01-16 19:59:30.841305", - "version": 0, - "url": "https://ge.xhamster.com/videos/group-sex-parties-3799842", - "review": 0, - "should_download": 0, - "title": "Gruppensex-Partys | xHamster", - "file_name": "Gruppensex-Partys [3799842].mp4", - "path": null, - "cloud_link": "/data/media/7128f33b-c370-4a30-a3fd-d68e6d925d52.mp4" - }, - { - "id": "713d1c70-d49e-4636-aff6-dfe55451616e", - "created_date": "2024-07-25 07:29:46.011939", - "last_modified_date": "2024-07-25 07:29:46.011939", - "version": 0, - "url": "https://ge.xhamster.com/videos/almost-virgin-two-teens-make-their-first-exchange-xhYnVxR", - "review": 0, - "should_download": 0, - "title": "Fast jungfr\u00e4ulich: Zwei Teenager machen ihren ersten Austausch! | xHamster", - "file_name": "Fast jungfr\u00e4ulich\uff1a Zwei Teenager machen ihren ersten Austausch! [xhYnVxR].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/713d1c70-d49e-4636-aff6-dfe55451616e.mp4" - }, - { - "id": "71874c7f-7c79-4b6f-942e-fa8105f94b5f", - "created_date": "2024-07-25 07:29:44.358918", - "last_modified_date": "2024-07-25 07:29:44.358918", - "version": 0, - "url": "https://ge.xhamster.com/videos/if-you-want-the-job-then-get-naked-fm14-131460", - "review": 0, - "should_download": 0, - "title": "Wenn Sie den Job wollen, dann nackt fm14 | xHamster", - "file_name": "Wenn Sie den Job wollen, dann nackt fm14 [131460].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/71874c7f-7c79-4b6f-942e-fa8105f94b5f.mp4" - }, - { - "id": "71a29116-ec7f-4929-b362-4fbb9262a72f", - "created_date": "2024-12-29 23:53:27.914657", - "last_modified_date": "2024-12-29 23:53:27.914657", - "version": 0, - "url": "https://ge.xhamster.com/videos/mommys-friend-is-as-slutty-as-her-12005465", - "review": 0, - "should_download": 0, - "title": "Mommys Freundin ist so versaut wie sie | xHamster", - "file_name": "Mommys Freundin ist so versaut wie sie [12005465].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/71a29116-ec7f-4929-b362-4fbb9262a72f.mp4" - }, - { - "id": "71c69654-9bfc-46b4-b5ee-1bca39a4e2b6", - "created_date": "2024-07-25 07:29:46.141002", - "last_modified_date": "2024-07-25 07:29:46.141002", - "version": 0, - "url": "https://ge.xhamster.com/videos/teenies-der-schule-full-movie-xhS1WDp", - "review": 0, - "should_download": 0, - "title": "Teenies Der Schule Full Movie, Free Big Cock HD Porn 22 | xHamster", - "file_name": "Teenies der Schule (Full Movie) [xhS1WDp].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/71c69654-9bfc-46b4-b5ee-1bca39a4e2b6.mp4" - }, - { - "id": "72465dbd-3013-4e3e-a2a1-266d1ec8eadd", - "created_date": "2024-07-25 07:29:46.949884", - "last_modified_date": "2024-07-25 07:29:46.949884", - "version": 0, - "url": "https://ge.xhamster.com/videos/stp4-lovely-daughter-fucks-two-of-her-step-dads-friends-7340165", - "review": 0, - "should_download": 0, - "title": "Stp4, sch\u00f6ne Tochter fickt zwei ihrer Stiefvater-Freunde! | xHamster", - "file_name": "Stp4, sch\u00f6ne Tochter fickt zwei ihrer Stiefvater-Freunde! [7340165].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/72465dbd-3013-4e3e-a2a1-266d1ec8eadd.mp4" - }, - { - "id": "7274c824-aa20-45e2-8e6f-85793f0856b7", - "created_date": "2024-10-07 20:47:56.415936", - "last_modified_date": "2024-10-21 16:29:37.747000", - "version": 2, - "url": "https://ge.xhamster.com/videos/naked-lunch-vintage-danish-anal-dp-fun-12801397", - "review": 0, - "should_download": 0, - "title": "Nacktes mittagessen: alter d\u00e4nischer anal-doppelpenetrationsspa\u00df | xHamster", - "file_name": "Nacktes mittagessen\uff1a alter d\u00e4nischer anal-doppelpenetrationsspa\u00df [12801397].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/7274c824-aa20-45e2-8e6f-85793f0856b7.mp4" - }, - { - "id": "72a5a42c-dcf8-40db-9e0b-ac65457c4c07", - "created_date": "2024-07-25 07:29:45.670114", - "last_modified_date": "2024-07-25 07:29:45.670114", - "version": 0, - "url": "https://ge.xhamster.com/videos/coed-sucks-dick-and-fucks-on-couch-xhZi1TM", - "review": 0, - "should_download": 0, - "title": "Coed lutscht Schwanz und fickt auf der Couch | xHamster", - "file_name": "Coed lutscht Schwanz und fickt auf der Couch [xhZi1TM].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/72a5a42c-dcf8-40db-9e0b-ac65457c4c07.mp4" - }, - { - "id": "734d7120-ba7d-4ccc-bc4b-c8042602071d", - "created_date": "2025-01-16 19:59:40.815410", - "last_modified_date": "2025-01-16 19:59:40.815416", - "version": 0, - "url": "https://ge.xhamster.com/videos/i-like-feeling-my-stepbrother-inside-me-xhsHVNu", - "review": 0, - "should_download": 0, - "title": "Ich mag es, meinen stiefbrud in mir zu f\u00fchlen | xHamster", - "file_name": "Ich mag es, meinen stiefbrud in mir zu f\u00fchlen [xhsHVNu].mp4", - "path": null, - "cloud_link": "/data/media/734d7120-ba7d-4ccc-bc4b-c8042602071d.mp4" - }, - { - "id": "7350506a-bde2-457a-993d-f513968cfd16", - "created_date": "2024-11-10 16:53:33.482203", - "last_modified_date": "2024-11-10 16:53:33.482203", - "version": 0, - "url": "https://ge.xhamster.com/videos/summer-wind-1-10518825", - "review": 0, - "should_download": 0, - "title": "Sommerwind 1 | xHamster", - "file_name": "Sommerwind 1 [10518825].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/7350506a-bde2-457a-993d-f513968cfd16.mp4" - }, - { - "id": "73a1bd4e-fa43-4ec9-ba3f-e9a976010f55", - "created_date": "2024-07-25 07:29:45.919244", - "last_modified_date": "2024-07-25 07:29:45.919244", - "version": 0, - "url": "https://ge.xhamster.com/videos/la-locandiera-parte-03-xhzkmFZ", - "review": 0, - "should_download": 0, - "title": "La Locandiera - Parte 03, Free Big Cock Porn cb | xHamster", - "file_name": "LA LOCANDIERA - Parte #03 [xhzkmFZ].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/73a1bd4e-fa43-4ec9-ba3f-e9a976010f55.mp4" - }, - { - "id": "73e38a46-c10c-4c22-9cdd-c54a88175df3", - "created_date": "2024-10-07 20:47:56.421616", - "last_modified_date": "2024-10-21 16:29:42.206000", - "version": 1, - "url": "https://ge.xhamster.com/videos/lost-bet-i-had-to-hold-out-my-ass-xhuECyX", - "review": 0, - "should_download": 0, - "title": "Wette verloren! Ich musste meinen Arsch hinhalten! | xHamster", - "file_name": "Wette verloren! Ich musste meinen Arsch hinhalten! [xhuECyX].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/73e38a46-c10c-4c22-9cdd-c54a88175df3.mp4" - }, - { - "id": "73f79729-410b-4aa5-9c6d-4825f3d4028f", - "created_date": "2024-07-25 07:29:45.017905", - "last_modified_date": "2024-07-25 07:29:45.017905", - "version": 0, - "url": "https://ge.xhamster.com/videos/forte-immerscharf6-6611934", - "review": 0, - "should_download": 0, - "title": "Forte Immerscharf6: Free Mature Porn Video e0 | xHamster", - "file_name": "FORTE Immerscharf6 [6611934].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/73f79729-410b-4aa5-9c6d-4825f3d4028f.mp4" - }, - { - "id": "741b0ae6-18c9-49e4-804c-7933c3393ef0", - "created_date": "2024-07-25 07:29:45.677150", - "last_modified_date": "2024-07-25 07:29:45.677150", - "version": 0, - "url": "https://ge.xhamster.com/videos/das-buch-der-sunde-xhwo6ny", - "review": 0, - "should_download": 0, - "title": "Das Buch Der Sunde: European HD Porn Video 8d | xHamster", - "file_name": "Das Buch Der Sunde [xhwo6ny].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/741b0ae6-18c9-49e4-804c-7933c3393ef0.mp4" - }, - { - "id": "742d81c2-1d3a-4dd2-9fca-67e94699552d", - "created_date": "2024-12-29 23:53:27.930890", - "last_modified_date": "2024-12-29 23:53:27.930890", - "version": 0, - "url": "https://ge.xhamster.com/videos/step-sisters-friend-likes-to-watch-s13-e6-xh8FhOx", - "review": 0, - "should_download": 0, - "title": "Die freundin der stiefschwester mag zuschauen - S13: e6 | xHamster", - "file_name": "Die freundin der stiefschwester mag zuschauen - S13\uff1a e6 [xh8FhOx].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/742d81c2-1d3a-4dd2-9fca-67e94699552d.mp4" - }, - { - "id": "745cc566-5e2e-4673-99a5-b5c0518711e4", - "created_date": "2024-07-25 07:29:44.958155", - "last_modified_date": "2024-07-25 07:29:44.958155", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-stepson-helps-me-cheat-and-get-back-at-my-husband-xhDB23m", - "review": 0, - "should_download": 0, - "title": "Mein Stiefsohn hilft mir, meinen Mann zu betr\u00fcgen und es ihm heimzuzahlen | xHamster", - "file_name": "Mein Stiefsohn hilft mir, meinen Mann zu betr\u00fcgen und es ihm heimzuzahlen [xhDB23m].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/745cc566-5e2e-4673-99a5-b5c0518711e4.mp4" - }, - { - "id": "74f94310-2be1-4977-bcc4-107f2634d56c", - "created_date": "2024-09-24 08:11:39.008109", - "last_modified_date": "2024-10-21 16:29:51.586000", - "version": 1, - "url": "https://ge.xhamster.com/videos/mommys-boy-horny-milf-penny-barber-is-caught-by-stepniece-chloe-surreal-while-fucking-her-stepson-xhlW34j", - "review": 0, - "should_download": 0, - "title": "Mommy's boy - die geile milf penny friseur wird von stiefnichte chloe surreal beim ficken ihres stiefsohns erwischt | xHamster", - "file_name": "Mommy's boy - die geile milf penny friseur wird von stiefnichte chloe surreal beim ficken ihres stiefsohns erwischt [xhlW34j].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/74f94310-2be1-4977-bcc4-107f2634d56c.mp4" - }, - { - "id": "750de205-7e84-4b61-adc9-d88ab6ea298a", - "created_date": "2024-07-25 07:29:46.528530", - "last_modified_date": "2024-07-25 07:29:46.528530", - "version": 0, - "url": "https://ge.xhamster.com/videos/i-need-a-dick-in-order-to-fuck-my-friend-can-you-help-adria-rae-begs-stepbro-s9-e7-xhXHcZr", - "review": 0, - "should_download": 0, - "title": "\"Ich brauche einen schwanz, um meinen freund zu ficken, kannst du helfen?\" Adria rae bettelt stiefbrud - s9: e7 | xHamster", - "file_name": "\uff02Ich brauche einen schwanz, um meinen freund zu ficken, kannst du helfen\uff1f\uff02 Adria rae bettelt stiefbrud - s9\uff1a e7 [xhXHcZr].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/750de205-7e84-4b61-adc9-d88ab6ea298a.mp4" - }, - { - "id": "75e166bb-f986-4177-80a0-7de3b06cf5f8", - "created_date": "2024-07-25 07:29:44.458838", - "last_modified_date": "2024-07-25 07:29:44.458838", - "version": 0, - "url": "https://ge.xhamster.com/videos/country-ficks-full-movie-xhSJAjH", - "review": 0, - "should_download": 0, - "title": "Land fickt (kompletter Film) | xHamster", - "file_name": "Land fickt (kompletter Film) [xhSJAjH].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/75e166bb-f986-4177-80a0-7de3b06cf5f8.mp4" - }, - { - "id": "760df509-51a6-452d-8f06-99dd27e91a22", - "created_date": "2024-12-29 23:53:27.923567", - "last_modified_date": "2024-12-29 23:53:27.923567", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-mischiefs-part-3-step-mom-and-step-daughter-teaching-bf-the-art-of-pleasure-xhitNGS", - "review": 0, - "should_download": 0, - "title": "Familien-unfug (teil 3): stiefmutter und stieftochter lehren freund die kunst des vergn\u00fcgens | xHamster", - "file_name": "Familien-unfug (teil 3)\uff1a stiefmutter und stieftochter lehren freund die kunst des vergn\u00fcgens [xhitNGS].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/760df509-51a6-452d-8f06-99dd27e91a22.mp4" - }, - { - "id": "7620ed7f-0620-47b3-9d2d-ba0fabe9c17f", - "created_date": "2024-07-25 07:29:44.388554", - "last_modified_date": "2024-07-25 07:29:44.388554", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-traditions-2-12951442", - "review": 0, - "should_download": 0, - "title": "Familientraditionen 2 | xHamster", - "file_name": "Familientraditionen 2 [12951442].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7620ed7f-0620-47b3-9d2d-ba0fabe9c17f.mp4" - }, - { - "id": "76275eea-b839-4065-8e6a-4fd84d73d6f0", - "created_date": "2024-07-25 07:29:45.086382", - "last_modified_date": "2024-07-25 07:29:45.086382", - "version": 0, - "url": "https://ge.xhamster.com/videos/busty-nala-brooks-tells-her-stepbro-my-boobs-are-you-serious-i-have-a-brain-s16-e2-xhgaoCo", - "review": 0, - "should_download": 0, - "title": "Die vollbusige Nala Brooks sagt ihrem Stiefbruder: \"Meine M\u00f6pse?! Meinst du das ernst, ich habe ein Gehirn!\" - s16: e2 | xHamster", - "file_name": "Die vollbusige Nala Brooks sagt ihrem Stiefbruder\uff1a \uff02Meine M\u00f6pse\uff1f! Meinst du das ernst, ich habe ein Gehirn!\uff02 - s16\uff1a e2 [xhgaoCo].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/76275eea-b839-4065-8e6a-4fd84d73d6f0.mp4" - }, - { - "id": "762a2cdc-3ce0-428d-a9db-5bc92a7ec7ea", - "created_date": "2024-07-25 07:29:47.866937", - "last_modified_date": "2024-07-25 07:29:47.866937", - "version": 0, - "url": "https://ge.xhamster.com/videos/18-and-confused-3-xhn7NA6", - "review": 0, - "should_download": 0, - "title": "18 und verwirrt # 3 | xHamster", - "file_name": "18 und verwirrt # 3 [xhn7NA6].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/762a2cdc-3ce0-428d-a9db-5bc92a7ec7ea.mp4" - }, - { - "id": "763272ee-bad7-42cf-84a9-fbfc04971997", - "created_date": "2024-07-25 07:29:45.256260", - "last_modified_date": "2024-07-25 07:29:45.256260", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepmom-says-i-have-a-better-place-for-your-cum-xhpNKGm", - "review": 0, - "should_download": 0, - "title": "Stiefmutter sagt, ich habe einen besseren Platz f\u00fcr dein Sperma! | xHamster", - "file_name": "Stiefmutter sagt, ich habe einen besseren Platz f\u00fcr dein Sperma! [xhpNKGm].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/763272ee-bad7-42cf-84a9-fbfc04971997.mp4" - }, - { - "id": "77113080-c397-459e-8637-b252a272b7bb", - "created_date": "2024-07-25 07:29:46.208773", - "last_modified_date": "2024-07-25 07:29:46.208773", - "version": 0, - "url": "https://ge.xhamster.com/videos/come-play-with-me-2-1980-restored-7566618", - "review": 0, - "should_download": 0, - "title": "Komm, spiel mit mir 2 - 1980 (restauriert) | xHamster", - "file_name": "Komm, spiel mit mir 2 - 1980 (restauriert) [7566618].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/77113080-c397-459e-8637-b252a272b7bb.mp4" - }, - { - "id": "771fea64-dc90-4d5e-b549-ccbbaadf914d", - "created_date": "2024-07-25 07:29:47.113723", - "last_modified_date": "2024-07-25 07:29:47.113723", - "version": 0, - "url": "https://ge.xhamster.com/videos/sb3-stepdaughter-gets-fucked-by-boyfriend-and-stepdad-6050058", - "review": 0, - "should_download": 0, - "title": "Sb3 Stieftochter wird von Freund und Stiefvater gefickt! | xHamster", - "file_name": "Sb3 Stieftochter wird von Freund und Stiefvater gefickt! [6050058].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/771fea64-dc90-4d5e-b549-ccbbaadf914d.mp4" - }, - { - "id": "777f2fc6-76db-4f5d-be24-3ee15f840a3e", - "created_date": "2024-07-25 07:29:47.036236", - "last_modified_date": "2024-07-25 07:29:47.036236", - "version": 0, - "url": "https://ge.xhamster.com/videos/alyx-star-her-bf-head-to-lauren-pixies-beauty-saloon-for-a-threesome-they-will-never-forget-brazzers-xh0Db0M", - "review": 0, - "should_download": 0, - "title": "Alyx spielt mit ihrem Freund in Lauren Pixies Beauty-Salon f\u00fcr einen Dreier, den sie nie vergessen werden - Brazzers | xHamster", - "file_name": "Alyx spielt mit ihrem Freund in Lauren Pixies Beauty-Salon f\u00fcr einen Dreier, den sie nie vergessen werden - Brazzers [xh0Db0M].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/777f2fc6-76db-4f5d-be24-3ee15f840a3e.mp4" - }, - { - "id": "7867291c-bbd1-40bb-9427-d3b44b9aa40a", - "created_date": "2024-07-25 07:29:47.665681", - "last_modified_date": "2024-07-25 07:29:47.665681", - "version": 0, - "url": "https://ge.xhamster.com/videos/atm-dp-babe-3way-double-ass-n-cuntfucked-by-big-cock-fellows-xhkvgFx", - "review": 0, - "should_download": 0, - "title": "ATM, DP, Sch\u00e4tzchen in Dreier, Doppelarsch und Fotze wird von Typen mit gro\u00dfem Schwanz gefickt | xHamster", - "file_name": "ATM, DP, Sch\u00e4tzchen in Dreier, Doppelarsch und Fotze wird von Typen mit gro\u00dfem Schwanz gefickt [xhkvgFx].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7867291c-bbd1-40bb-9427-d3b44b9aa40a.mp4" - }, - { - "id": "787d5400-f765-4b2f-ab42-6ab4e5bab944", - "created_date": "2024-07-25 07:29:45.082938", - "last_modified_date": "2024-07-25 07:29:45.082938", - "version": 0, - "url": "https://ge.xhamster.com/videos/potters-girlfriend-gets-hard-dick-in-face-while-babysitting-xhw7ckY", - "review": 0, - "should_download": 0, - "title": "Potters Freundin bekommt beim Babysitten einen harten Schwanz ins Gesicht | xHamster", - "file_name": "Potters Freundin bekommt beim Babysitten einen harten Schwanz ins Gesicht [xhw7ckY].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/787d5400-f765-4b2f-ab42-6ab4e5bab944.mp4" - }, - { - "id": "789dd021-a760-4457-a856-2d8b557d3db5", - "created_date": "2024-07-25 07:29:46.918873", - "last_modified_date": "2024-07-25 07:29:46.918873", - "version": 0, - "url": "https://ge.xhamster.com/videos/small-tit-blondie-comes-across-three-horny-men-and-she-gives-xhFwh0v", - "review": 0, - "should_download": 0, - "title": "Blondine mit kleinen Titten trifft auf drei geile M\u00e4nner\u2026 | xHamster", - "file_name": "Blondine mit kleinen Titten trifft auf drei geile M\u00e4nner\u2026 [xhFwh0v].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/789dd021-a760-4457-a856-2d8b557d3db5.mp4" - }, - { - "id": "78b9cc10-6a97-4267-bb42-c575def86f43", - "created_date": "2024-07-25 07:29:47.484073", - "last_modified_date": "2024-07-25 07:29:47.484073", - "version": 0, - "url": "https://ge.xhamster.com/videos/drncm-classic-dp-b24-xhUvIS6", - "review": 0, - "should_download": 0, - "title": "Drncm Klassiker, Doppelpenetration B24 | xHamster", - "file_name": "Drncm Klassiker, Doppelpenetration B24 [xhUvIS6].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/78b9cc10-6a97-4267-bb42-c575def86f43.mp4" - }, - { - "id": "7947f803-84cd-4baa-b34e-e5f4883f2505", - "created_date": "2024-12-29 23:53:27.930013", - "last_modified_date": "2024-12-29 23:53:27.930013", - "version": 0, - "url": "https://ge.xhamster.com/videos/runaway-step-niece-gets-treated-like-a-personal-freeuse-sex-slave-by-her-step-auntie-her-husband-xhleYMJ", - "review": 0, - "should_download": 0, - "title": "Runaway stiefnichte wird von ihrer stieftante und ihrem ehemann wie eine pers\u00f6nliche freie sexsklavin behandelt | xHamster", - "file_name": "Runaway stiefnichte wird von ihrer stieftante und ihrem ehemann wie eine pers\u00f6nliche freie sexsklavin behandelt [xhleYMJ].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/7947f803-84cd-4baa-b34e-e5f4883f2505.mp4" - }, - { - "id": "79e8b6bd-46e6-4559-8729-c7e6c7f45b1c", - "created_date": "2024-07-25 07:29:44.677857", - "last_modified_date": "2024-07-25 07:29:44.677857", - "version": 0, - "url": "https://ge.xhamster.com/videos/perfekte-reitschlampen-full-movie-xhiBqHy", - "review": 0, - "should_download": 0, - "title": "Perfekte Reitschlampen Full Movie, Free Porn da | xHamster", - "file_name": "Perfekte Reitschlampen (Full Movie) [xhiBqHy].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/79e8b6bd-46e6-4559-8729-c7e6c7f45b1c.mp4" - }, - { - "id": "79ec65b5-aaf3-4cc2-8ad0-1df51d326641", - "created_date": "2024-07-25 07:29:48.055937", - "last_modified_date": "2024-07-25 07:29:48.055937", - "version": 0, - "url": "https://ge.xhamster.com/videos/sexy-cute-stepsister-loves-petting-from-me-karolinaorgasm-xhgTZ62", - "review": 0, - "should_download": 0, - "title": "Sexy s\u00fc\u00dfe Stiefschwester liebt es, von mir zu streicheln - Karolinaorgasm | xHamster", - "file_name": "Sexy s\u00fc\u00dfe Stiefschwester liebt es, von mir zu streicheln - Karolinaorgasm [xhgTZ62].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/79ec65b5-aaf3-4cc2-8ad0-1df51d326641.mp4" - }, - { - "id": "79ee0fc8-5058-484c-86fa-5b12fb2557ae", - "created_date": "2024-07-25 07:29:44.392061", - "last_modified_date": "2024-07-25 07:29:44.392061", - "version": 0, - "url": "https://ge.xhamster.com/videos/i-heat-up-voyeurs-at-the-beach-and-end-up-full-of-cum-xhUGRI1", - "review": 0, - "should_download": 0, - "title": "Ich w\u00e4rme Voyeure am Strand auf und lande voller Sperma, | xHamster", - "file_name": "Ich w\u00e4rme Voyeure am Strand auf und lande voller Sperma, [xhUGRI1].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/79ee0fc8-5058-484c-86fa-5b12fb2557ae.mp4" - }, - { - "id": "79febbb5-ae59-4898-a368-1866359a0b8d", - "created_date": "2024-12-29 23:53:27.913574", - "last_modified_date": "2024-12-29 23:53:27.913574", - "version": 0, - "url": "https://ge.xhamster.com/videos/just-another-taboo-orgy-at-the-house-11102874", - "review": 0, - "should_download": 0, - "title": "Nur eine weitere Tabu-Orgie im Haus | xHamster", - "file_name": "Nur eine weitere Tabu-Orgie im Haus [11102874].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/79febbb5-ae59-4898-a368-1866359a0b8d.mp4" - }, - { - "id": "7a4258fb-08d4-47a5-80ee-60e5083839ca", - "created_date": "2024-08-28 23:21:54.371401", - "last_modified_date": "2024-08-28 23:21:54.371401", - "version": 0, - "url": "https://ge.xhamster.com/videos/bffs-summer-camp-counselors-record-lesbian-orgy-4972921", - "review": 0, - "should_download": 0, - "title": "Bffs - Sommercamp-Beraterinnen nehmen lesbische Orgien auf | xHamster", - "file_name": "Bffs - Sommercamp-Beraterinnen nehmen lesbische Orgien auf [4972921].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/7a4258fb-08d4-47a5-80ee-60e5083839ca.mp4" - }, - { - "id": "7a82fafa-4d5e-456c-b7aa-815c8d850d7a", - "created_date": "2024-07-25 07:29:44.861437", - "last_modified_date": "2024-07-25 07:29:44.861437", - "version": 0, - "url": "https://ge.xhamster.com/videos/private-private-com-hot-ellen-betsy-fucks-her-way-to-cheap-house-xhcsO0j", - "review": 0, - "should_download": 0, - "title": "Private.com - die hei\u00dfe Ellen Betsy fickt sich zum billigen Haus! | xHamster", - "file_name": "Private.com - die hei\u00dfe Ellen Betsy fickt sich zum billigen Haus! [xhcsO0j].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/7a82fafa-4d5e-456c-b7aa-815c8d850d7a.mp4" - }, - { - "id": "7a8c8d8c-3fd4-4582-b661-9221e6f157c4", - "created_date": "2024-07-25 07:29:45.639991", - "last_modified_date": "2024-07-25 07:29:45.639991", - "version": 0, - "url": "https://ge.xhamster.com/videos/die-wette-2007-german-tyra-misoux-dvd-xhGNmvY", - "review": 0, - "should_download": 0, - "title": "Die Wette 2007 German Tyra Misoux Dvd, HD Porn 67 | xHamster", - "file_name": "Die Wette (2007, German, Tyra Misoux, DVD) [xhGNmvY].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7a8c8d8c-3fd4-4582-b661-9221e6f157c4.mp4" - }, - { - "id": "7a9c6680-d48f-47fa-b0e9-f71f974321eb", - "created_date": "2024-10-07 20:47:56.416898", - "last_modified_date": "2024-10-21 16:29:59.363000", - "version": 1, - "url": "https://ge.xhamster.com/videos/first-threesome-anal-sex-and-dp-for-lilith-xhJi0oC", - "review": 0, - "should_download": 0, - "title": "Erster dreier, analsex und doppelpenetration f\u00fcr lilith | xHamster", - "file_name": "Erster dreier, analsex und doppelpenetration f\u00fcr lilith [xhJi0oC].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/7a9c6680-d48f-47fa-b0e9-f71f974321eb.mp4" - }, - { - "id": "7aec1ac9-caf7-40c7-846d-707196ec4c8b", - "created_date": "2024-07-25 07:29:47.502456", - "last_modified_date": "2024-07-25 07:29:47.502456", - "version": 0, - "url": "https://ge.xhamster.com/videos/interracial-hippie-orgies-1976-12159586", - "review": 0, - "should_download": 0, - "title": "Interracial Hippie-Orgien (1976) | xHamster", - "file_name": "Interracial Hippie-Orgien (1976) [12159586].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7aec1ac9-caf7-40c7-846d-707196ec4c8b.mp4" - }, - { - "id": "7b098a86-2e68-4742-976f-d6dc17eae2d7", - "created_date": "2024-07-25 07:29:46.881634", - "last_modified_date": "2024-07-25 07:29:46.881634", - "version": 0, - "url": "https://ge.xhamster.com/videos/business-trip-sex-with-naughty-secretary-business-bitch-xhKXDQi", - "review": 0, - "should_download": 0, - "title": "Gesch\u00e4ftsreisen-Sex mit frechen Sekret\u00e4rin, Business-Schlampe | xHamster", - "file_name": "Gesch\u00e4ftsreisen-Sex mit frechen Sekret\u00e4rin, Business-Schlampe [xhKXDQi].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7b098a86-2e68-4742-976f-d6dc17eae2d7.mp4" - }, - { - "id": "7b14c2d0-4913-425f-af47-1f04ba6ff0b3", - "created_date": "2024-07-25 07:29:48.176365", - "last_modified_date": "2024-07-25 07:29:48.176365", - "version": 0, - "url": "https://ge.xhamster.com/videos/passion-hd-motivated-assistant-fucks-her-boss-for-raise-xhNeGygY", - "review": 0, - "should_download": 0, - "title": "Passion-hd motivierte Assistentin fickt ihren Chef f\u00fcr Gehaltserh\u00f6hung | xHamster", - "file_name": "Passion-hd motivierte Assistentin fickt ihren Chef f\u00fcr Gehaltserh\u00f6hung [xhNeGygY].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7b14c2d0-4913-425f-af47-1f04ba6ff0b3.mp4" - }, - { - "id": "7b87eace-162b-4ebc-8080-616fd20dd1bf", - "created_date": "2024-07-25 07:29:48.151451", - "last_modified_date": "2024-07-25 07:29:48.151451", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-fun-921359", - "review": 0, - "should_download": 0, - "title": "Familienspa\u00df | xHamster", - "file_name": "Familienspa\u00df [921359].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7b87eace-162b-4ebc-8080-616fd20dd1bf.mp4" - }, - { - "id": "7bde1b95-498b-4a90-a079-990d5ec0e023", - "created_date": "2025-01-17 16:34:37.854825", - "last_modified_date": "2025-01-17 16:34:37.854831", - "version": 0, - "url": "https://ge.xhamster.com/videos/how-to-play-the-game-s40-e4-xhKgE88", - "review": 0, - "should_download": 0, - "title": "Wie man das Spiel spielt - s40: e4 | xHamster", - "file_name": "Wie man das Spiel spielt - s40\uff1a e4 [xhKgE88].mp4", - "path": null, - "cloud_link": "/data/media/7bde1b95-498b-4a90-a079-990d5ec0e023.mp4" - }, - { - "id": "7bea39b5-5324-4640-9f5a-a339ce321110", - "created_date": "2024-07-25 07:29:47.725840", - "last_modified_date": "2024-07-25 07:29:47.725840", - "version": 0, - "url": "https://ge.xhamster.com/videos/whitney-aj-alexis-and-tony-unleash-their-sexual-tensions-after-the-game-xh9QfP3", - "review": 0, - "should_download": 0, - "title": "Whitney, aj, Alexis und Tony l\u00f6sen ihre sexuellen Spannungen nach dem Spiel | xHamster", - "file_name": "Whitney, aj, Alexis und Tony l\u00f6sen ihre sexuellen Spannungen nach dem Spiel [xh9QfP3].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7bea39b5-5324-4640-9f5a-a339ce321110.mp4" - }, - { - "id": "7c0a109e-f581-454f-b05c-c793784484d8", - "created_date": "2024-07-25 07:29:46.503687", - "last_modified_date": "2024-07-25 07:29:46.503687", - "version": 0, - "url": "https://ge.xhamster.com/videos/fun-and-games-with-step-sis-xhDT2Uy", - "review": 0, - "should_download": 0, - "title": "Spa\u00df und Spiele mit Stiefschwester | xHamster", - "file_name": "Spa\u00df und Spiele mit Stiefschwester [xhDT2Uy].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7c0a109e-f581-454f-b05c-c793784484d8.mp4" - }, - { - "id": "7c2f7c8c-dda5-4371-9a9c-657392ef3c5d", - "created_date": "2024-07-25 07:29:46.111905", - "last_modified_date": "2024-07-25 07:29:46.111905", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsis-says-i-just-want-to-see-what-kind-of-porn-you-watch-xhbALMG", - "review": 0, - "should_download": 0, - "title": "Stiefschwester sagt, ich will nur sehen, welche Art von Porno du dir ansiehst! | xHamster", - "file_name": "Stiefschwester sagt, ich will nur sehen, welche Art von Porno du dir ansiehst! [xhbALMG].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7c2f7c8c-dda5-4371-9a9c-657392ef3c5d.mp4" - }, - { - "id": "7cc35476-a522-4b5e-a22c-523b32140d50", - "created_date": "2024-07-25 07:29:48.105231", - "last_modified_date": "2024-07-25 07:29:48.105231", - "version": 0, - "url": "https://ge.xhamster.com/videos/sex-geschichten-heisser-braute-full-hd-movie-original-xhnymJw", - "review": 0, - "should_download": 0, - "title": "Sex Geschichten Heisser Braute - Full HD Movie - Original | xHamster", - "file_name": "SEX GESCHICHTEN HEISSER BRAUTE - (Full HD Movie - Original [xhnymJw].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7cc35476-a522-4b5e-a22c-523b32140d50.mp4" - }, - { - "id": "7cdf1a74-b863-4af8-9f92-aafda41ccd0c", - "created_date": "2024-07-25 07:29:47.261523", - "last_modified_date": "2024-07-25 07:29:47.261523", - "version": 0, - "url": "https://ge.xhamster.com/videos/3-way-porn-big-boat-group-sex-party-part-2-13327664", - "review": 0, - "should_download": 0, - "title": "3-Wege-Porno - Gruppensex-Party mit gro\u00dfem Boot - Teil 2 | xHamster", - "file_name": "3-Wege-Porno - Gruppensex-Party mit gro\u00dfem Boot - Teil 2 [13327664].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7cdf1a74-b863-4af8-9f92-aafda41ccd0c.mp4" - }, - { - "id": "7d20b1c1-98e4-4eae-a524-68d692485170", - "created_date": "2024-07-25 07:29:47.284085", - "last_modified_date": "2024-07-25 07:29:47.284085", - "version": 0, - "url": "https://ge.xhamster.com/videos/nudist-darts-squirt-my-plan-didnt-work-out-over-bullseye-in-my-pussy-xhw0vJE", - "review": 0, - "should_download": 0, - "title": "FKK DARTS SQUIRT ORGIE! Mein Plan ging nicht auf! \u00dcbers Bullseye in meine Muschi! | xHamster", - "file_name": "FKK DARTS SQUIRT ORGIE! Mein Plan ging nicht auf! \u00dcbers Bullseye in meine Muschi! [xhw0vJE].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7d20b1c1-98e4-4eae-a524-68d692485170.mp4" - }, - { - "id": "7d213f63-0333-4d5a-a9db-35fa31573fde", - "created_date": "2024-07-25 07:29:45.865717", - "last_modified_date": "2024-07-25 07:29:45.865717", - "version": 0, - "url": "https://ge.xhamster.com/videos/daily-stories-of-young-sluts-full-movie-xhOVI3v", - "review": 0, - "should_download": 0, - "title": "T\u00e4gliche geschichten von jungen schlampen - KOMPLETTER FILM | xHamster", - "file_name": "T\u00e4gliche geschichten von jungen schlampen - KOMPLETTER FILM [xhOVI3v].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7d213f63-0333-4d5a-a9db-35fa31573fde.mp4" - }, - { - "id": "7d65d3d8-ddf6-44b4-887d-bfdc6d7b4256", - "created_date": "2024-07-25 07:29:46.679494", - "last_modified_date": "2024-07-25 07:29:46.679494", - "version": 0, - "url": "https://ge.xhamster.com/videos/college-spex-teen-facialized-in-dormroom-orgy-7793905", - "review": 0, - "should_download": 0, - "title": "College-Spex-Teen in der Schlafsaal-Orgie ins Gesicht gespritzt | xHamster", - "file_name": "College-Spex-Teen in der Schlafsaal-Orgie ins Gesicht gespritzt [7793905].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7d65d3d8-ddf6-44b4-887d-bfdc6d7b4256.mp4" - }, - { - "id": "7dbed932-89bd-41a0-9ca6-d3762c56e0ee", - "created_date": "2024-12-29 23:53:27.897986", - "last_modified_date": "2024-12-29 23:53:27.897986", - "version": 0, - "url": "https://ge.xhamster.com/videos/milf-giving-my-redhead-stepmom-the-creampie-she-deserves-13366137", - "review": 0, - "should_download": 0, - "title": "Milf, meiner rothaarigen Stiefmutter den Creampie geben, den sie verdient | xHamster", - "file_name": "Milf, meiner rothaarigen Stiefmutter den Creampie geben, den sie verdient [13366137].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/7dbed932-89bd-41a0-9ca6-d3762c56e0ee.mp4" - }, - { - "id": "7e389768-55ad-4413-82a5-070c5c1b5e20", - "created_date": "2024-07-25 07:29:45.278060", - "last_modified_date": "2024-07-25 07:29:45.278060", - "version": 0, - "url": "https://ge.xhamster.com/videos/gangbang-in-der-kueche-xhxCRXO", - "review": 0, - "should_download": 0, - "title": "Gangbang in Der Kueche, Free Brutal Sex Porn f2 | xHamster", - "file_name": "Gangbang in der Kueche [xhxCRXO].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7e389768-55ad-4413-82a5-070c5c1b5e20.mp4" - }, - { - "id": "7e3b65c7-ee77-4682-b94f-2cd592e80e4f", - "created_date": "2024-07-25 07:29:47.964894", - "last_modified_date": "2024-07-25 07:29:47.964894", - "version": 0, - "url": "https://ge.xhamster.com/videos/behind-the-scenes-at-our-office-14839388", - "review": 0, - "should_download": 0, - "title": "Hinter den Kulissen in unserem B\u00fcro | xHamster", - "file_name": "Hinter den Kulissen in unserem B\u00fcro [14839388].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7e3b65c7-ee77-4682-b94f-2cd592e80e4f.mp4" - }, - { - "id": "7e3befc3-04a9-4ab5-bdc5-0493de4d972b", - "created_date": "2024-07-25 07:29:45.494986", - "last_modified_date": "2024-07-25 07:29:45.494986", - "version": 0, - "url": "https://ge.xhamster.com/videos/perverse-minds-episode-01-xhThHGmF", - "review": 0, - "should_download": 0, - "title": "Perverse K\u00f6pfe !!! - Episode # 01 | xHamster", - "file_name": "Perverse K\u00f6pfe !!! - Episode # 01 [xhThHGmF].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7e3befc3-04a9-4ab5-bdc5-0493de4d972b.mp4" - }, - { - "id": "7e51ab34-fd4f-425f-b33e-8391d8fdb7d1", - "created_date": "2025-01-19 13:42:28.707422", - "last_modified_date": "2025-01-19 13:42:28.707429", - "version": 0, - "url": "https://ge.xhamster.com/videos/plenty-for-everyone-s45-e18-xhvXGBg", - "review": 0, - "should_download": 0, - "title": "Viel f\u00fcr jeden - S45: e18 | xHamster", - "file_name": "Viel f\u00fcr jeden - S45\uff1a e18 [xhvXGBg].mp4", - "path": null, - "cloud_link": null - }, - { - "id": "7e9bc820-6593-4efd-91ee-d67b2a0e34ea", - "created_date": "2024-07-25 07:29:45.438595", - "last_modified_date": "2024-07-25 07:29:45.438595", - "version": 0, - "url": "https://ge.xhamster.com/videos/amazing-french-blonde-milf-double-penetrated-at-the-beach-xhODl8H", - "review": 0, - "should_download": 0, - "title": "Erstaunliche franz\u00f6sische blonde MILF am strand doppelt penetriert | xHamster", - "file_name": "Erstaunliche franz\u00f6sische blonde MILF am strand doppelt penetriert [xhODl8H].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/7e9bc820-6593-4efd-91ee-d67b2a0e34ea.mp4" - }, - { - "id": "7ed3d5c0-11eb-401f-8965-206bffc0fa0d", - "created_date": "2024-08-28 23:21:54.350667", - "last_modified_date": "2024-08-28 23:21:54.350667", - "version": 0, - "url": "https://ge.xhamster.com/videos/wife-fucked-in-poker-game-12677727", - "review": 0, - "should_download": 0, - "title": "Ehefrau im Pokerspiel gefickt | xHamster", - "file_name": "Ehefrau im Pokerspiel gefickt [12677727].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/7ed3d5c0-11eb-401f-8965-206bffc0fa0d.mp4" - }, - { - "id": "7f0be3ba-f222-4540-9fdf-472a74a1524a", - "created_date": "2025-01-19 13:42:36.947901", - "last_modified_date": "2025-01-19 13:42:36.947908", - "version": 0, - "url": "https://ge.xhamster.com/videos/oops-my-stepmom-tripped-on-my-dick-again-jane-cane-shiny-cock-films-xhI8LhZ", - "review": 0, - "should_download": 0, - "title": "Ups, meine stiefmutter hat auf meinen schwanz gestopft! Wieder! Jane rohrstock, gl\u00e4nzende schwanzfilme | xHamster", - "file_name": "Ups, meine stiefmutter hat auf meinen schwanz gestopft! Wieder! Jane rohrstock, gl\u00e4nzende schwanzfilme [xhI8LhZ].mp4", - "path": null, - "cloud_link": null - }, - { - "id": "7f345d7f-51c1-4d9d-bf8f-79517a2da36c", - "created_date": "2025-01-17 16:34:28.620435", - "last_modified_date": "2025-01-17 16:34:28.620442", - "version": 0, - "url": "https://ge.xhamster.com/videos/catrice-xhAyCw2", - "review": 0, - "should_download": 0, - "title": "Catrice: Retro HD Porn Video ce | xHamster", - "file_name": "Catrice [xhAyCw2].mp4", - "path": null, - "cloud_link": "/data/media/7f345d7f-51c1-4d9d-bf8f-79517a2da36c.mp4" - }, - { - "id": "7f64a874-ae3d-4840-870c-a4677a919b3f", - "created_date": "2024-07-25 07:29:47.129060", - "last_modified_date": "2024-07-25 07:29:47.129060", - "version": 0, - "url": "https://ge.xhamster.com/videos/thick-new-secretary-holly-day-learns-the-free-use-office-rules-on-her-first-day-freeuse-fantasy-xhER46f", - "review": 0, - "should_download": 0, - "title": "Dicke neue sekret\u00e4rin Holly day lernt an ihrem ersten tag die freien b\u00fcroregeln - freeUse fantasy | xHamster", - "file_name": "Dicke neue sekret\u00e4rin Holly day lernt an ihrem ersten tag die freien b\u00fcroregeln - freeUse fantasy [xhER46f].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7f64a874-ae3d-4840-870c-a4677a919b3f.mp4" - }, - { - "id": "7f7b06ca-a46c-480e-bec5-c8866099a933", - "created_date": "2024-07-25 07:29:46.200150", - "last_modified_date": "2024-07-25 07:29:46.200150", - "version": 0, - "url": "https://ge.xhamster.com/videos/teenievision-22-schluckende-schwanz-goren-a-noi-piace-succhiare-den-frechen-goren-juckt-der-arsch-xhqx8Vz", - "review": 0, - "should_download": 0, - "title": "Teenievision 22 - Schluckende Schwanz-goren - a Noi Piace Succhiare - Den Frechen Goren Juckt Der Arsch | xHamster", - "file_name": "TeenieVision 22 - Schluckende Schwanz-Goren - A noi piace succhiare - Den frechen Goren juckt der Arsch [xhqx8Vz].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7f7b06ca-a46c-480e-bec5-c8866099a933.mp4" - }, - { - "id": "7f8ffb98-92b3-4ca6-8b40-44ae03bbb801", - "created_date": "2024-07-25 07:29:46.776290", - "last_modified_date": "2024-07-25 07:29:46.776290", - "version": 0, - "url": "https://ge.xhamster.com/videos/american-pure-pleasure-xxx-vol-04-xhizSQr", - "review": 0, - "should_download": 0, - "title": "Amerikanisch pures Vergn\u00fcgen xxx - vol. # 04 | xHamster", - "file_name": "Amerikanisch pures Vergn\u00fcgen xxx - vol. # 04 [xhizSQr].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7f8ffb98-92b3-4ca6-8b40-44ae03bbb801.mp4" - }, - { - "id": "7fa6fd73-6391-4ada-8eaf-b8a73cc01bc8", - "created_date": "2024-07-25 07:29:47.224581", - "last_modified_date": "2024-07-25 07:29:47.224581", - "version": 0, - "url": "https://ge.xhamster.com/videos/step-mom-fucked-by-stepson-xhaHIvE", - "review": 0, - "should_download": 0, - "title": "Stiefmutter vom Stiefsohn gefickt | xHamster", - "file_name": "Stiefmutter vom Stiefsohn gefickt [xhaHIvE].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/7fa6fd73-6391-4ada-8eaf-b8a73cc01bc8.mp4" - }, - { - "id": "7ffcc84e-df73-4254-acd6-675fabd29d42", - "created_date": "2024-08-16 12:29:27.802000", - "last_modified_date": "2024-10-21 16:30:07.121000", - "version": 1, - "url": "https://ge.xhamster.com/videos/perverted-sex-games-1977-xhNUlrP", - "review": 0, - "should_download": 0, - "title": "Perverse Sexspiele (1977) | xHamster", - "file_name": "Perverse Sexspiele (1977) [xhNUlrP].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/7ffcc84e-df73-4254-acd6-675fabd29d42.mp4" - }, - { - "id": "8056eaa6-04e3-40ad-a8ad-be2e0bf57516", - "created_date": "2024-07-25 07:29:46.874117", - "last_modified_date": "2024-07-25 07:29:46.874117", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsiblings-orgy-fuck-in-front-of-step-mom-myfamilypies-s3-e4-10114327", - "review": 0, - "should_download": 0, - "title": "Stiefgeschwister-Orgie ficken vor Stiefmutter - myfamilypies s3: e4 | xHamster", - "file_name": "Stiefgeschwister-Orgie ficken vor Stiefmutter - myfamilypies s3\uff1a e4 [10114327].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8056eaa6-04e3-40ad-a8ad-be2e0bf57516.mp4" - }, - { - "id": "80fe6acd-b32b-45d1-9ba1-e189a41b43cf", - "created_date": "2024-11-10 16:53:33.486690", - "last_modified_date": "2024-11-10 16:53:33.486690", - "version": 0, - "url": "https://ge.xhamster.com/videos/after-the-wedding-2017-xhn5MhG", - "review": 0, - "should_download": 0, - "title": "Nach der Hochzeit 2017 | xHamster", - "file_name": "Nach der Hochzeit 2017 [xhn5MhG].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/80fe6acd-b32b-45d1-9ba1-e189a41b43cf.mp4" - }, - { - "id": "812e57ff-4ec1-4315-9f26-876f0fb8b739", - "created_date": "2024-07-25 07:29:47.506055", - "last_modified_date": "2024-07-25 07:29:47.506055", - "version": 0, - "url": "https://ge.xhamster.com/videos/classic-artist-s-tale-xhbNuX4", - "review": 0, - "should_download": 0, - "title": "Klassische K\u00fcnstlergeschichte | xHamster", - "file_name": "Klassische K\u00fcnstlergeschichte [xhbNuX4].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/812e57ff-4ec1-4315-9f26-876f0fb8b739.mp4" - }, - { - "id": "81dc2252-aefe-4f0d-8084-5a16523fda0c", - "created_date": "2024-07-25 07:29:47.307387", - "last_modified_date": "2024-07-25 07:29:47.307387", - "version": 0, - "url": "https://ge.xhamster.com/videos/swimming-pool-orgy-at-czech-mega-swingers-1081009", - "review": 0, - "should_download": 0, - "title": "Schwimmbad-Orgie bei tschechischen Mega-Swingern | xHamster", - "file_name": "Schwimmbad-Orgie bei tschechischen Mega-Swingern [1081009].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/81dc2252-aefe-4f0d-8084-5a16523fda0c.mp4" - }, - { - "id": "8204a76a-be81-45a1-9847-e2cbdcbfc7e2", - "created_date": "2024-07-25 07:29:45.316467", - "last_modified_date": "2024-07-25 07:29:45.316467", - "version": 0, - "url": "https://ge.xhamster.com/videos/griechische-liebesnaechte-1984-9092728", - "review": 0, - "should_download": 0, - "title": "Griechische Liebesnaechte 1984, Free Teen Porn a9 | xHamster", - "file_name": "Griechische Liebesnaechte (1984) [9092728].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8204a76a-be81-45a1-9847-e2cbdcbfc7e2.mp4" - }, - { - "id": "8227283d-7c12-4894-bd63-8c0fa29f6837", - "created_date": "2024-09-24 08:11:39.000933", - "last_modified_date": "2024-10-21 16:30:14.035000", - "version": 1, - "url": "https://ge.xhamster.com/videos/family-strokes-buxom-bombshell-milf-joins-her-stepson-and-his-sexy-gf-while-banging-on-the-couch-xhWhtRz", - "review": 0, - "should_download": 0, - "title": "In Family Strokes, einer drallen MILF, schlie\u00dft sich ihr MILF ihrem Stiefsohn und seiner sexy Freundin an, w\u00e4hrend sie auf der Couch knallt | xHamster", - "file_name": "In Family Strokes, einer drallen MILF, schlie\u00dft sich ihr MILF ihrem Stiefsohn und seiner sexy Freundin an, w\u00e4hrend sie auf der Couch knallt [xhWhtRz].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/8227283d-7c12-4894-bd63-8c0fa29f6837.mp4" - }, - { - "id": "824b0de1-6db2-43f6-a8e9-7948014c5df8", - "created_date": "2024-07-25 07:29:47.631349", - "last_modified_date": "2024-07-25 07:29:47.631349", - "version": 0, - "url": "https://ge.xhamster.com/videos/american-college-xxx-full-movie-hd-original-version-xhzrtLPK", - "review": 0, - "should_download": 0, - "title": "American College xxx - (kompletter Film hd Originalversion) | xHamster", - "file_name": "American College xxx - (kompletter Film hd Originalversion) [xhzrtLPK].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/824b0de1-6db2-43f6-a8e9-7948014c5df8.mp4" - }, - { - "id": "829a359b-e36b-4018-8f20-e91de3b53592", - "created_date": "2024-07-25 07:29:45.522181", - "last_modified_date": "2024-07-25 07:29:45.522181", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-hot-blowjobs-of-the-naughty-nurse-xhYi9qF", - "review": 0, - "should_download": 0, - "title": "Die hei\u00dfen Blowjobs der frechen Krankenschwester | xHamster", - "file_name": "Die hei\u00dfen Blowjobs der frechen Krankenschwester [xhYi9qF].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/829a359b-e36b-4018-8f20-e91de3b53592.mp4" - }, - { - "id": "83e7f55f-a8ad-4ebf-b60f-d41ce157d97c", - "created_date": "2024-08-28 23:21:54.360773", - "last_modified_date": "2024-08-28 23:21:54.360773", - "version": 0, - "url": "https://ge.xhamster.com/videos/young-guy-gambles-his-cute-teen-girlfriend-at-gambling-xhtuSZ2", - "review": 0, - "should_download": 0, - "title": "Junger Typ verzockt seine niedliche Teenie Freundin beim Gl\u00fccksspiel | xHamster", - "file_name": "Junger Typ verzockt seine niedliche Teenie Freundin beim Gl\u00fccksspiel [xhtuSZ2].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/83e7f55f-a8ad-4ebf-b60f-d41ce157d97c.mp4" - }, - { - "id": "83f7b829-4210-4f66-bc1a-b3c1e1ad3122", - "created_date": "2024-08-09 21:05:39.819412", - "last_modified_date": "2024-08-16 10:31:14.624000", - "version": 1, - "url": "https://ge.xhamster.com/videos/sneaky-voyeurs-jenga-game-gets-naughty-seduction-leads-to-intense-finale-xhTTHna", - "review": 0, - "should_download": 0, - "title": "Hinterh\u00e4ltige voyeurs: Jenga-Spiel wird frech, Verf\u00fchrung f\u00fchrt zu intensivem finale | xHamster", - "file_name": "Hinterh\u00e4ltige voyeurs\uff1a Jenga-Spiel wird frech, Verf\u00fchrung f\u00fchrt zu intensivem finale [xhTTHna].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/83f7b829-4210-4f66-bc1a-b3c1e1ad3122.mp4" - }, - { - "id": "83fabb38-655b-41ad-b4ef-ce4acbdb29e2", - "created_date": "2024-07-25 07:29:46.242353", - "last_modified_date": "2024-07-25 07:29:46.242353", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepmom-helps-me-loosen-up-s8-e9-xhR8r1n", - "review": 0, - "should_download": 0, - "title": "Stiefmutter hilft mir, mich zu lockern - s8: e9 | xHamster", - "file_name": "Stiefmutter hilft mir, mich zu lockern - s8\uff1a e9 [xhR8r1n].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/83fabb38-655b-41ad-b4ef-ce4acbdb29e2.mp4" - }, - { - "id": "841e00c7-abb2-47c4-b978-52a87bbdde2b", - "created_date": "2024-07-25 07:29:45.415535", - "last_modified_date": "2024-07-25 07:29:45.415535", - "version": 0, - "url": "https://ge.xhamster.com/videos/beer-pong-and-blowjobs-1480121", - "review": 0, - "should_download": 0, - "title": "Bier-Pong und Blowjobs | xHamster", - "file_name": "Bier-Pong und Blowjobs [1480121].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/841e00c7-abb2-47c4-b978-52a87bbdde2b.mp4" - }, - { - "id": "842c8c66-2a4e-40fc-b50b-cd946e6dd218", - "created_date": "2024-07-25 07:29:48.048519", - "last_modified_date": "2024-07-25 07:29:48.048519", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-little-stepsister-loves-to-suck-my-big-cock-karolinaorgasm-xhnEhiE", - "review": 0, - "should_download": 0, - "title": "Meine kleine Stiefschwester liebt es, meinen gro\u00dfen Schwanz zu lutschen - Karolinaorgasm | xHamster", - "file_name": "Meine kleine Stiefschwester liebt es, meinen gro\u00dfen Schwanz zu lutschen - Karolinaorgasm [xhnEhiE].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/842c8c66-2a4e-40fc-b50b-cd946e6dd218.mp4" - }, - { - "id": "843622b2-1f8b-43a9-8b57-f51fcd62de81", - "created_date": "2024-07-25 07:29:45.064070", - "last_modified_date": "2024-07-25 07:29:45.064070", - "version": 0, - "url": "https://ge.xhamster.com/videos/forte-immerscharf8-6612071", - "review": 0, - "should_download": 0, - "title": "Forte Immerscharf8: Free Mature Porn Video 70 | xHamster", - "file_name": "FORTE Immerscharf8 [6612071].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/843622b2-1f8b-43a9-8b57-f51fcd62de81.mp4" - }, - { - "id": "846c7325-aaff-4a46-856a-090668ef826d", - "created_date": "2024-07-25 07:29:44.684934", - "last_modified_date": "2024-07-25 07:29:44.684934", - "version": 0, - "url": "https://ge.xhamster.com/videos/perversum-chateau-14260395", - "review": 0, - "should_download": 0, - "title": "Perversum Chateau: Free European Porn Video 8e | xHamster", - "file_name": "Perversum Chateau [14260395].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/846c7325-aaff-4a46-856a-090668ef826d.mp4" - }, - { - "id": "84708b5f-0822-4dc8-a499-407c10f92246", - "created_date": "2024-07-25 07:29:46.817533", - "last_modified_date": "2024-07-25 07:29:46.817533", - "version": 0, - "url": "https://ge.xhamster.com/videos/nasty-german-housewives-xhM0cH3", - "review": 0, - "should_download": 0, - "title": "Versaute deutsche Hausfrauen | xHamster", - "file_name": "Versaute deutsche Hausfrauen [xhM0cH3].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/84708b5f-0822-4dc8-a499-407c10f92246.mp4" - }, - { - "id": "852a058b-731b-4c7f-8616-c245865e4303", - "created_date": "2024-07-25 07:29:45.449697", - "last_modified_date": "2024-07-25 07:29:45.449697", - "version": 0, - "url": "https://ge.xhamster.com/videos/teenie-parade-15-full-movie-xh7GDGk", - "review": 0, - "should_download": 0, - "title": "Teenie-Parade 15 (kompletter Film) | xHamster", - "file_name": "Teenie-Parade 15 (kompletter Film) [xh7GDGk].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/852a058b-731b-4c7f-8616-c245865e4303.mp4" - }, - { - "id": "853a389d-c1f7-43cd-a4d6-e99aa6cba67c", - "created_date": "2024-07-25 07:29:45.040235", - "last_modified_date": "2024-07-25 07:29:45.040235", - "version": 0, - "url": "https://ge.xhamster.com/videos/literaturquintett-nicht-ohne-meine-locher-1995-xhFxThW", - "review": 0, - "should_download": 0, - "title": "Literaturquintett Nicht Ohne Meine Locher 1995: Porn 11 | xHamster", - "file_name": "Literaturquintett\uff1a Nicht ohne meine Locher (1995) [xhFxThW].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/853a389d-c1f7-43cd-a4d6-e99aa6cba67c.mp4" - }, - { - "id": "85c2f039-d5d0-463a-bfaf-392ee0b7b51a", - "created_date": "2024-09-24 08:11:39.006101", - "last_modified_date": "2024-10-21 16:30:19.990000", - "version": 1, - "url": "https://ge.xhamster.com/videos/finally-18-the-first-time-xhcsjFh", - "review": 0, - "should_download": 0, - "title": "Endlich 18! Das erste Mal! | xHamster", - "file_name": "Endlich 18! Das erste Mal! [xhcsjFh].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/85c2f039-d5d0-463a-bfaf-392ee0b7b51a.mp4" - }, - { - "id": "85d7b763-dd13-4afa-a082-ae2873391a61", - "created_date": "2024-11-01 21:08:26.937271", - "last_modified_date": "2024-11-01 21:08:26.937271", - "version": 0, - "url": "https://ge.xhamster.com/videos/die-arschfickwirtin-4613032", - "review": 0, - "should_download": 0, - "title": "Die Arschfickwirtin: Free Anal Porn Video a8 | xHamster", - "file_name": "Die Arschfickwirtin [4613032].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/85d7b763-dd13-4afa-a082-ae2873391a61.mp4" - }, - { - "id": "85f9b91a-fbff-44d8-bf94-04e147f66016", - "created_date": "2024-07-25 07:29:46.718820", - "last_modified_date": "2024-07-25 07:29:46.718820", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsis-says-bet-you-never-thought-your-stepmom-would-be-sucking-your-cock-xhm3g2n", - "review": 0, - "should_download": 0, - "title": "Stiefschwester sagt, wetten, du h\u00e4ttest nie gedacht, dass deine Stiefmutter deinen Schwanz lutscht | xHamster", - "file_name": "Stiefschwester sagt, wetten, du h\u00e4ttest nie gedacht, dass deine Stiefmutter deinen Schwanz lutscht [xhm3g2n].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/85f9b91a-fbff-44d8-bf94-04e147f66016.mp4" - }, - { - "id": "86027361-7d40-488e-be07-b013ec5756bb", - "created_date": "2024-07-25 07:29:45.547650", - "last_modified_date": "2024-07-25 07:29:45.547650", - "version": 0, - "url": "https://ge.xhamster.com/videos/horny-hot-milf-cory-chase-empties-her-stepson-s-cock-xhj5cc5", - "review": 0, - "should_download": 0, - "title": "Geile hei\u00dfe MILF Cory Chase leert den Schwanz ihres Stiefsohns | xHamster", - "file_name": "Geile hei\u00dfe MILF Cory Chase leert den Schwanz ihres Stiefsohns [xhj5cc5].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/86027361-7d40-488e-be07-b013ec5756bb.mp4" - }, - { - "id": "861ea1f3-c3b2-47c5-ae48-2af2715d23a9", - "created_date": "2024-12-30 18:49:39.874000", - "last_modified_date": "2025-01-03 01:46:10.678000", - "version": 2, - "url": "https://ge.xhamster.com/videos/stepbrother-and-stepsister-seduce-drunk-stepmom-into-threesome-14171473", - "review": 0, - "should_download": 0, - "title": "Stiefbruder und Schwester verf\u00fchren Mutter zum Dreier | xHamster", - "file_name": "Stiefbruder und Schwester verf\u00fchren Mutter zum Dreier [14171473].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/861ea1f3-c3b2-47c5-ae48-2af2715d23a9.mp4" - }, - { - "id": "8656dc50-8003-4a4d-90b7-063633bc7a05", - "created_date": "2024-07-25 07:29:44.624306", - "last_modified_date": "2024-07-25 07:29:44.624306", - "version": 0, - "url": "https://ge.xhamster.com/videos/old-german-porn-9756751", - "review": 0, - "should_download": 0, - "title": "Alter deutscher Porno. | xHamster", - "file_name": "Alter deutscher Porno. [9756751].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8656dc50-8003-4a4d-90b7-063633bc7a05.mp4" - }, - { - "id": "865ed219-bcf0-45c6-b160-e11c5efdf115", - "created_date": "2024-07-25 07:29:45.375122", - "last_modified_date": "2024-07-25 07:29:45.375122", - "version": 0, - "url": "https://ge.xhamster.com/videos/all-made-in-family-xhPaPlS", - "review": 0, - "should_download": 0, - "title": "Alles in familie gemacht | xHamster", - "file_name": "Alles in familie gemacht [xhPaPlS].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/865ed219-bcf0-45c6-b160-e11c5efdf115.mp4" - }, - { - "id": "86704785-3a27-4ffc-81c9-f218cfef6522", - "created_date": "2024-07-25 07:29:44.875893", - "last_modified_date": "2024-07-25 07:29:44.875893", - "version": 0, - "url": "https://ge.xhamster.com/videos/happy-birthday-capri-xhsvSxW", - "review": 0, - "should_download": 0, - "title": "Alles Gute zum Geburtstag, Capri | xHamster", - "file_name": "Alles Gute zum Geburtstag, Capri [xhsvSxW].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/86704785-3a27-4ffc-81c9-f218cfef6522.mp4" - }, - { - "id": "8690d732-3709-485f-9c7d-98f9367c5f72", - "created_date": "2024-07-25 07:29:47.862846", - "last_modified_date": "2024-07-25 07:29:47.862846", - "version": 0, - "url": "https://ge.xhamster.com/videos/wie-rettet-man-eine-ehe-1976-with-patricia-rhomberg-6182432", - "review": 0, - "should_download": 0, - "title": "Wie Rettet Man Eine Ehe 1976 with Patricia Rhomberg | xHamster", - "file_name": "Wie Rettet Man Eine Ehe (1976) with Patricia Rhomberg [6182432].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8690d732-3709-485f-9c7d-98f9367c5f72.mp4" - }, - { - "id": "86cd7994-6c59-4714-aecb-1a89d778f168", - "created_date": "2024-07-25 07:29:45.839405", - "last_modified_date": "2024-07-25 07:29:45.839405", - "version": 0, - "url": "https://ge.xhamster.com/videos/step-mom-got-stretched-out-by-3-great-whites-11405640", - "review": 0, - "should_download": 0, - "title": "Stiefmutter wurde von 3 tollen Wei\u00dfen gestreckt | xHamster", - "file_name": "Stiefmutter wurde von 3 tollen Wei\u00dfen gestreckt [11405640].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/86cd7994-6c59-4714-aecb-1a89d778f168.mp4" - }, - { - "id": "8731317c-2251-44db-88a5-94cf1ad12f85", - "created_date": "2024-09-05 20:03:45.850222", - "last_modified_date": "2024-10-21 16:30:24.505000", - "version": 1, - "url": "https://ge.xhamster.com/videos/classic-eighties-vintage-11-4049136", - "review": 0, - "should_download": 0, - "title": "Klassischer Retro der 80er Jahre | xHamster", - "file_name": "Klassischer Retro der 80er Jahre [4049136].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/8731317c-2251-44db-88a5-94cf1ad12f85.mp4" - }, - { - "id": "873da7af-13e8-4c2e-850c-1c04bc21383e", - "created_date": "2024-07-25 07:29:47.854553", - "last_modified_date": "2024-07-25 07:29:47.854553", - "version": 0, - "url": "https://ge.xhamster.com/videos/perversion-in-ibiza-full-movie-original-in-full-hd-xhJHHlR", - "review": 0, - "should_download": 0, - "title": "Perversion in Ibiza - (kompletter Film) - (Original in Full HD) | xHamster", - "file_name": "Perversion in Ibiza - (kompletter Film) - (Original in Full HD) [xhJHHlR].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/873da7af-13e8-4c2e-850c-1c04bc21383e.mp4" - }, - { - "id": "87662bfa-edd2-4377-956a-49562f67213a", - "created_date": "2024-07-25 07:29:44.562585", - "last_modified_date": "2024-07-25 07:29:44.562585", - "version": 0, - "url": "https://ge.xhamster.com/videos/familystrokes-stepfamily-taboo-orgy-with-stepdaughter-judy-jolie-and-busty-stepmom-becky-bandini-xh6VzN1", - "review": 0, - "should_download": 0, - "title": "Familystrokes - Stieffamilien-Tabu-Orgie mit Stieftochter Judy Jolie und vollbusiger Stiefmutter Becky Bandini | xHamster", - "file_name": "Familystrokes - Stieffamilien-Tabu-Orgie mit Stieftochter Judy Jolie und vollbusiger Stiefmutter Becky Bandini [xh6VzN1].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/87662bfa-edd2-4377-956a-49562f67213a.mp4" - }, - { - "id": "87b25060-41f8-40bd-98aa-7b0733ab930f", - "created_date": "2024-08-28 23:21:54.363542", - "last_modified_date": "2024-08-28 23:21:54.363542", - "version": 0, - "url": "https://ge.xhamster.com/videos/it-s-good-having-slutty-friends-to-fuck-with-xhW3zI4", - "review": 0, - "should_download": 0, - "title": "Es ist gut, mit versauten Freunden zu ficken | xHamster", - "file_name": "Es ist gut, mit versauten Freunden zu ficken [xhW3zI4].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/87b25060-41f8-40bd-98aa-7b0733ab930f.mp4" - }, - { - "id": "87f2e8af-fe90-45b6-ab80-56c6595cc3ae", - "created_date": "2024-07-25 07:29:47.642946", - "last_modified_date": "2024-07-25 07:29:47.642946", - "version": 0, - "url": "https://ge.xhamster.com/videos/vintage-orgy-138-11414198", - "review": 0, - "should_download": 0, - "title": "Retro-Orgie 138 | xHamster", - "file_name": "Retro-Orgie 138 [11414198].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/87f2e8af-fe90-45b6-ab80-56c6595cc3ae.mp4" - }, - { - "id": "88054bed-48a6-46d2-b8e8-b22546689627", - "created_date": "2025-01-17 16:34:33.254526", - "last_modified_date": "2025-01-17 16:34:33.254532", - "version": 0, - "url": "https://ge.xhamster.com/videos/naomi-walker-step-mom-and-daugther-fuck-by-3-studs-10238796", - "review": 0, - "should_download": 0, - "title": "In Naomi Walker ficken Stiefmutter und Tochter von 3 Hengsten | xHamster", - "file_name": "In Naomi Walker ficken Stiefmutter und Tochter von 3 Hengsten [10238796].mp4", - "path": null, - "cloud_link": "/data/media/88054bed-48a6-46d2-b8e8-b22546689627.mp4" - }, - { - "id": "8836c945-da32-49ed-aa3b-fca5fb75dcf9", - "created_date": "2024-07-25 07:29:47.775844", - "last_modified_date": "2024-07-25 07:29:47.775844", - "version": 0, - "url": "https://ge.xhamster.com/videos/busty-alexis-fawx-fucking-her-boss-in-the-office-14838792", - "review": 0, - "should_download": 0, - "title": "Die vollbusige Alexis Fawx fickt ihren Chef im B\u00fcro | xHamster", - "file_name": "Die vollbusige Alexis Fawx fickt ihren Chef im B\u00fcro [14838792].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8836c945-da32-49ed-aa3b-fca5fb75dcf9.mp4" - }, - { - "id": "88564351-085c-4335-8e26-5dd807409b2c", - "created_date": "2024-07-25 07:29:47.093079", - "last_modified_date": "2024-07-25 07:29:47.093079", - "version": 0, - "url": "https://ge.xhamster.com/videos/boarding-school-full-movie-xhndz9Q", - "review": 0, - "should_download": 0, - "title": "Internat (kompletter Film) | xHamster", - "file_name": "Internat (kompletter Film) [xhndz9Q].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/88564351-085c-4335-8e26-5dd807409b2c.mp4" - }, - { - "id": "8915e465-8442-4557-b917-4f6deb6289ec", - "created_date": "2024-07-25 07:29:45.518428", - "last_modified_date": "2024-07-25 07:29:45.518428", - "version": 0, - "url": "https://ge.xhamster.com/videos/little-stepbrother-caught-wanking-and-deflowered-xhXevD6", - "review": 0, - "should_download": 0, - "title": "Kleiner Bruder beim Wichsen erwischt und entjungfert | xHamster", - "file_name": "Kleiner Bruder beim Wichsen erwischt und entjungfert [xhXevD6].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8915e465-8442-4557-b917-4f6deb6289ec.mp4" - }, - { - "id": "89617489-2e4d-49e3-b5e1-ed475fdc9e3a", - "created_date": "2024-07-25 07:29:45.613085", - "last_modified_date": "2024-07-25 07:29:45.613085", - "version": 0, - "url": "https://ge.xhamster.com/videos/wife-gets-woken-up-by-husband-and-his-friend-then-fucks-both-of-them-xhePvGw", - "review": 0, - "should_download": 0, - "title": "Die Ehefrau wird von Ehemann und seinem Freund geweckt und fickt dann beide | xHamster", - "file_name": "Die Ehefrau wird von Ehemann und seinem Freund geweckt und fickt dann beide [xhePvGw].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/89617489-2e4d-49e3-b5e1-ed475fdc9e3a.mp4" - }, - { - "id": "89d5a590-fe2d-404e-914a-89b0a024db45", - "created_date": "2024-07-25 07:29:45.149988", - "last_modified_date": "2024-07-25 07:29:45.149988", - "version": 0, - "url": "https://ge.xhamster.com/videos/stief-mutter-erwischt-tante-mit-sohn-und-fickt-mit-8899974", - "review": 0, - "should_download": 0, - "title": "Stief-mutter Erwischt Tante Mit Sohn Und Fickt Mit: Porn 54 | xHamster", - "file_name": "Stief-Mutter erwischt Tante mit Sohn und fickt mit [8899974].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/89d5a590-fe2d-404e-914a-89b0a024db45.mp4" - }, - { - "id": "89ea68d6-8de3-4dda-aefc-41e1a1371a2c", - "created_date": "2024-07-25 07:29:44.681415", - "last_modified_date": "2024-07-25 07:29:44.681415", - "version": 0, - "url": "https://ge.xhamster.com/videos/casey-calvert-gets-assbanged-by-black-guys-5309556", - "review": 0, - "should_download": 0, - "title": "Casey Calvert wird von Schwarzen gefickt | xHamster", - "file_name": "Casey Calvert wird von Schwarzen gefickt [5309556].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/89ea68d6-8de3-4dda-aefc-41e1a1371a2c.mp4" - }, - { - "id": "8a75a960-02d8-4ade-abff-e889725b117e", - "created_date": "2024-07-25 07:29:47.616386", - "last_modified_date": "2024-07-25 07:29:47.616386", - "version": 0, - "url": "https://ge.xhamster.com/videos/urlaubsflirts-full-movie-xhMVuEm", - "review": 0, - "should_download": 0, - "title": "Urlaubsflirts Full Movie, Free Story HD Porn 07 | xHamster", - "file_name": "Urlaubsflirts (Full Movie) [xhMVuEm].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8a75a960-02d8-4ade-abff-e889725b117e.mp4" - }, - { - "id": "8b046e5d-3d6b-4380-816e-69b6e516b78e", - "created_date": "2024-12-29 23:53:27.901150", - "last_modified_date": "2024-12-29 23:53:27.901150", - "version": 0, - "url": "https://ge.xhamster.com/videos/grenzenloser-deutscher-sex-full-movie-xhlEt9E", - "review": 0, - "should_download": 0, - "title": "Grenzenloser Deutscher Sex Full Movie, Porn 05 | xHamster", - "file_name": "Grenzenloser deutscher Sex! (Full Movie) [xhlEt9E].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/8b046e5d-3d6b-4380-816e-69b6e516b78e.mp4" - }, - { - "id": "8b47ac62-1c85-4747-a84b-0e448464c4d6", - "created_date": "2024-07-25 07:29:44.586155", - "last_modified_date": "2024-07-25 07:29:44.586155", - "version": 0, - "url": "https://ge.xhamster.com/videos/mother-gets-dp-from-two-young-boys-xhLJ0aU", - "review": 0, - "should_download": 0, - "title": "Mutter bekommt Doppelpenetration von zwei kleinen Jungs | xHamster", - "file_name": "Mutter bekommt Doppelpenetration von zwei kleinen Jungs [xhLJ0aU].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8b47ac62-1c85-4747-a84b-0e448464c4d6.mp4" - }, - { - "id": "8b846460-0a3f-48a6-90a9-bcd91081ff0f", - "created_date": "2024-07-25 07:29:47.889399", - "last_modified_date": "2024-07-25 07:29:47.889399", - "version": 0, - "url": "https://ge.xhamster.com/videos/nzest-skandale-verbotene-10656719", - "review": 0, - "should_download": 0, - "title": "nzest Skandale Verbotene | xHamster", - "file_name": "nzest Skandale Verbotene [10656719].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8b846460-0a3f-48a6-90a9-bcd91081ff0f.mp4" - }, - { - "id": "8b9c77e3-45bf-4446-b4f9-1e7cd63fa28a", - "created_date": "2024-07-25 07:29:44.658867", - "last_modified_date": "2024-07-25 07:29:44.658867", - "version": 0, - "url": "https://ge.xhamster.com/videos/sex-teacher-julia-ann-fucking-4185459", - "review": 0, - "should_download": 0, - "title": "Sexlehrerin Julia Ann fickt | xHamster", - "file_name": "Sexlehrerin Julia Ann fickt [4185459].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8b9c77e3-45bf-4446-b4f9-1e7cd63fa28a.mp4" - }, - { - "id": "8bc8ddd0-81cd-4a5a-8394-368b5e7ea3ee", - "created_date": "2024-07-25 07:29:46.399811", - "last_modified_date": "2024-07-25 07:29:46.399811", - "version": 0, - "url": "https://ge.xhamster.com/videos/joi-roommate-s42-e16-xhgSjgG", - "review": 0, - "should_download": 0, - "title": "Wichsanleitung, WG - s42: e16 | xHamster", - "file_name": "Wichsanleitung, WG - s42\uff1a e16 [xhgSjgG].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8bc8ddd0-81cd-4a5a-8394-368b5e7ea3ee.mp4" - }, - { - "id": "8bd420af-f78d-48a5-ae66-e31ed5cea46d", - "created_date": "2024-07-25 07:29:45.532774", - "last_modified_date": "2024-07-25 07:29:45.532774", - "version": 0, - "url": "https://ge.xhamster.com/videos/familien-sex-schatten-der-bluschande-2190111", - "review": 0, - "should_download": 0, - "title": "Familien Sex Schatten Der Bluschande, Porn 2c | xHamster", - "file_name": "Familien Sex Schatten Der Bluschande [2190111].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/8bd420af-f78d-48a5-ae66-e31ed5cea46d.mp4" - }, - { - "id": "8bef162a-1814-4603-8b86-79ccb4f1ef6b", - "created_date": "2024-09-24 08:11:39.008415", - "last_modified_date": "2024-10-21 16:30:29.995000", - "version": 1, - "url": "https://ge.xhamster.com/videos/sons-friend-gives-the-milf-a-surprise-xh8lb1G", - "review": 0, - "should_download": 0, - "title": "Der Freund des Sohnes \u00fcberrascht die MILF | xHamster", - "file_name": "Der Freund des Sohnes \u00fcberrascht die MILF [xh8lb1G].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/8bef162a-1814-4603-8b86-79ccb4f1ef6b.mp4" - }, - { - "id": "8c27327e-6931-4b17-8ced-2dc1203c6e6d", - "created_date": "2024-07-25 07:29:46.994188", - "last_modified_date": "2024-07-25 07:29:46.994188", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-thighs-10437025", - "review": 0, - "should_download": 0, - "title": "Familienschenkel | xHamster", - "file_name": "Familienschenkel [10437025].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8c27327e-6931-4b17-8ced-2dc1203c6e6d.mp4" - }, - { - "id": "8c76da37-7205-4fcb-abad-58f0867a0fe7", - "created_date": "2024-07-25 07:29:45.142775", - "last_modified_date": "2024-09-06 09:41:16.095000", - "version": 1, - "url": "https://ge.xhamster.com/videos/verbotene-familien-spiele-1257-7093171", - "review": 0, - "should_download": 0, - "title": "Verbotene Familien spiele", - "file_name": "Verbotene Familien spiele 1257 [7093171].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8c76da37-7205-4fcb-abad-58f0867a0fe7.mp4" - }, - { - "id": "8cefff34-0c21-46bc-85ae-775aab0d5be8", - "created_date": "2024-07-25 07:29:45.055855", - "last_modified_date": "2024-07-25 07:29:45.055855", - "version": 0, - "url": "https://ge.xhamster.com/videos/mutti-ist-die-beste-3227797", - "review": 0, - "should_download": 0, - "title": "Mutti Ist Die Beste: Free Taboo Porn Video cd | xHamster", - "file_name": "Mutti ist die Beste [3227797].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8cefff34-0c21-46bc-85ae-775aab0d5be8.mp4" - }, - { - "id": "8dc5f613-6c04-4b68-8077-df82638cbbd0", - "created_date": "2024-07-25 07:29:44.775762", - "last_modified_date": "2024-07-25 07:29:44.775762", - "version": 0, - "url": "https://ge.xhamster.com/videos/familystrokes-hot-big-tit-aunt-helps-me-cum-7579336", - "review": 0, - "should_download": 0, - "title": "Familystrokes - eine hei\u00dfe Tante mit dicken Titten hilft mir beim Abspritzen | xHamster", - "file_name": "Familystrokes - eine hei\u00dfe Tante mit dicken Titten hilft mir beim Abspritzen [7579336].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8dc5f613-6c04-4b68-8077-df82638cbbd0.mp4" - }, - { - "id": "8dddecb4-0da7-4cb3-954a-3eaccd8910f7", - "created_date": "2024-07-25 07:29:44.910423", - "last_modified_date": "2024-07-25 07:29:44.910423", - "version": 0, - "url": "https://ge.xhamster.com/videos/18-videoz-threesome-with-two-cumshots-7129909", - "review": 0, - "should_download": 0, - "title": "18 Videoz - Dreier mit zwei Cumshots | xHamster", - "file_name": "18 Videoz - Dreier mit zwei Cumshots [7129909].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8dddecb4-0da7-4cb3-954a-3eaccd8910f7.mp4" - }, - { - "id": "8e672eaf-cf60-4f1f-817f-fef92bd940dc", - "created_date": "2024-10-21 15:08:43.556675", - "last_modified_date": "2024-10-21 16:30:34.407000", - "version": 1, - "url": "https://ge.xhamster.com/videos/shes-for-lunch-6244676", - "review": 0, - "should_download": 0, - "title": "Sie ist zum Mittagessen | xHamster", - "file_name": "Sie ist zum Mittagessen [6244676].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/8e672eaf-cf60-4f1f-817f-fef92bd940dc.mp4" - }, - { - "id": "8e7f890d-f124-480e-b020-8b7e8cce7caa", - "created_date": "2024-07-25 07:29:47.109941", - "last_modified_date": "2024-07-25 07:29:47.109941", - "version": 0, - "url": "https://ge.xhamster.com/videos/husband-watches-wife-get-fucked-by-a-stranger-xhsD91B", - "review": 0, - "should_download": 0, - "title": "Ehemann beobachtet, wie seine Ehefrau von einem Fremden gefickt wird | xHamster", - "file_name": "Ehemann beobachtet, wie seine Ehefrau von einem Fremden gefickt wird [xhsD91B].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8e7f890d-f124-480e-b020-8b7e8cce7caa.mp4" - }, - { - "id": "8e7fcbb7-fcfc-43f6-815d-7697d782c213", - "created_date": "2024-07-25 07:29:47.847145", - "last_modified_date": "2024-07-25 07:29:47.847145", - "version": 0, - "url": "https://ge.xhamster.com/videos/familienausflug-auf-dem-campingplatz-10805450", - "review": 0, - "should_download": 0, - "title": "Familienausflug Auf Dem Campingplatz, Porn 23 | xHamster", - "file_name": "Familienausflug auf dem Campingplatz [10805450].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8e7fcbb7-fcfc-43f6-815d-7697d782c213.mp4" - }, - { - "id": "8edd1307-9ee7-4a5f-860a-f9ff643f9192", - "created_date": "2024-07-25 07:29:44.484260", - "last_modified_date": "2024-07-25 07:29:44.484260", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepdads-teach-their-stepdaughters-how-to-fuck-daughterswap-xhpj6m7", - "review": 0, - "should_download": 0, - "title": "Stiefv\u00e4ter bringen ihren stieft\u00f6chtern bei, wie man fickt - daughterswap | xHamster", - "file_name": "Stiefv\u00e4ter bringen ihren stieft\u00f6chtern bei, wie man fickt - daughterswap [xhpj6m7].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/8edd1307-9ee7-4a5f-860a-f9ff643f9192.mp4" - }, - { - "id": "8eed03f1-b536-4451-ab44-699b51865857", - "created_date": "2024-07-25 07:29:48.037433", - "last_modified_date": "2024-07-25 07:29:48.037433", - "version": 0, - "url": "https://ge.xhamster.com/videos/tushy-first-double-penetration-for-natasha-nice-6106099", - "review": 0, - "should_download": 0, - "title": "Tushy erste Doppelpenetration f\u00fcr Natasha sch\u00f6n | xHamster", - "file_name": "Tushy erste Doppelpenetration f\u00fcr Natasha sch\u00f6n [6106099].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8eed03f1-b536-4451-ab44-699b51865857.mp4" - }, - { - "id": "8efc0364-2ebd-4195-b9c8-8938be499018", - "created_date": "2024-07-25 07:29:47.081447", - "last_modified_date": "2024-07-25 07:29:47.081447", - "version": 0, - "url": "https://ge.xhamster.com/videos/wife-in-a-bikini-gets-unprotected-creampie-and-facial-from-two-cocks-on-public-beach-xhIfw4p", - "review": 0, - "should_download": 0, - "title": "Ehefrau im Bikini bekommt ungesch\u00fctzten Creampie und Gesichtsbesamung von zwei Schw\u00e4nzen am \u00f6ffentlichen Strand | xHamster", - "file_name": "Ehefrau im Bikini bekommt ungesch\u00fctzten Creampie und Gesichtsbesamung von zwei Schw\u00e4nzen am \u00f6ffentlichen Strand [xhIfw4p].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8efc0364-2ebd-4195-b9c8-8938be499018.mp4" - }, - { - "id": "8f334780-aeb5-4ff3-a8c1-804a0ab93967", - "created_date": "2024-08-28 23:21:54.362667", - "last_modified_date": "2024-08-28 23:21:54.362667", - "version": 0, - "url": "https://ge.xhamster.com/videos/caught-fucking-by-their-parents-12844706", - "review": 0, - "should_download": 0, - "title": "Erwischt beim Ficken von ihren Eltern | xHamster", - "file_name": "Erwischt beim Ficken von ihren Eltern [12844706].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/8f334780-aeb5-4ff3-a8c1-804a0ab93967.mp4" - }, - { - "id": "8f82d5b7-7c90-4572-8b1b-848fa3e2c728", - "created_date": "2024-10-21 15:08:43.551517", - "last_modified_date": "2024-10-21 16:30:40.419000", - "version": 1, - "url": "https://ge.xhamster.com/videos/schoolgirls-scene-2-xhgz8nu", - "review": 0, - "should_download": 0, - "title": "Schulm\u00e4dchen-Szene 2 | xHamster", - "file_name": "Schulm\u00e4dchen-Szene 2 [xhgz8nu].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/8f82d5b7-7c90-4572-8b1b-848fa3e2c728.mp4" - }, - { - "id": "8facb33e-47ea-4372-b4f1-183cc741503f", - "created_date": "2024-07-25 07:29:45.079307", - "last_modified_date": "2024-07-25 07:29:45.079307", - "version": 0, - "url": "https://ge.xhamster.com/videos/slut-gets-her-asshole-drilled-in-the-garden-xhMbmOr", - "review": 0, - "should_download": 0, - "title": "Schlampe bekommt im Garten ihr Arschloch gebohrt | xHamster", - "file_name": "Schlampe bekommt im Garten ihr Arschloch gebohrt [xhMbmOr].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/8facb33e-47ea-4372-b4f1-183cc741503f.mp4" - }, - { - "id": "911de070-7a37-406a-a3bd-5ecf27a02e3e", - "created_date": "2024-07-25 07:29:47.124985", - "last_modified_date": "2024-07-25 07:29:47.124985", - "version": 0, - "url": "https://ge.xhamster.com/videos/2-stunden-buro-miezen-full-movie-xhf51Xk", - "review": 0, - "should_download": 0, - "title": "2 Stunden Buro Miezen Full Movie, Free HD Porn 1a | xHamster", - "file_name": "2 Stunden Buro Miezen (Full Movie) [xhf51Xk].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/911de070-7a37-406a-a3bd-5ecf27a02e3e.mp4" - }, - { - "id": "9138f276-692e-40af-8e24-d672df7e1118", - "created_date": "2024-07-25 07:29:45.813081", - "last_modified_date": "2024-07-25 07:29:45.813081", - "version": 0, - "url": "https://ge.xhamster.com/videos/great-threesome-3994235", - "review": 0, - "should_download": 0, - "title": "Toller Dreier | xHamster", - "file_name": "Toller Dreier [3994235].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9138f276-692e-40af-8e24-d672df7e1118.mp4" - }, - { - "id": "91ab647c-23a0-4021-95f3-fee357b26d12", - "created_date": "2024-07-25 07:29:44.395646", - "last_modified_date": "2024-07-25 07:29:44.395646", - "version": 0, - "url": "https://ge.xhamster.com/videos/models-auf-dem-prufstand-1999-german-full-video-dvd-rip-xhEAfHe", - "review": 0, - "should_download": 0, - "title": "Models Auf Dem Prufstand 1999 German Full Video Dvd Rip | xHamster", - "file_name": "Models auf dem Prufstand (1999, German, full video, DVD rip) [xhEAfHe].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/91ab647c-23a0-4021-95f3-fee357b26d12.mp4" - }, - { - "id": "91c77fcb-65fc-4312-95e4-d49db05511b4", - "created_date": "2024-07-25 07:29:46.659646", - "last_modified_date": "2024-07-25 07:29:46.659646", - "version": 0, - "url": "https://ge.xhamster.com/videos/special-order-higher-quality-xhXhSkP", - "review": 0, - "should_download": 0, - "title": "Sonderbestellung (h\u00f6here Qualit\u00e4t) | xHamster", - "file_name": "Sonderbestellung (h\u00f6here Qualit\u00e4t) [xhXhSkP].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/91c77fcb-65fc-4312-95e4-d49db05511b4.mp4" - }, - { - "id": "9265488d-e97e-4272-84f3-3310e5d88ef6", - "created_date": "2024-07-25 07:29:45.109037", - "last_modified_date": "2024-07-25 07:29:45.109037", - "version": 0, - "url": "https://ge.xhamster.com/videos/bratty-sis-messing-with-stepsis-and-my-cock-slips-in-s4-e2-9673701", - "review": 0, - "should_download": 0, - "title": "Brattige Schwester spielt mit Stiefschwester und mein Schwanz rutscht rein! s4: e2 | xHamster", - "file_name": "Brattige Schwester spielt mit Stiefschwester und mein Schwanz rutscht rein! s4\uff1a e2 [9673701].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9265488d-e97e-4272-84f3-3310e5d88ef6.mp4" - }, - { - "id": "9268a65a-d371-42aa-8990-29ea3e192c0c", - "created_date": "2024-07-25 07:29:45.101934", - "last_modified_date": "2024-07-25 07:29:45.101934", - "version": 0, - "url": "https://ge.xhamster.com/videos/backyard-summer-orgy-xh8Kuh2", - "review": 0, - "should_download": 0, - "title": "Hinterhof-Sommer-Orgie | xHamster", - "file_name": "Hinterhof-Sommer-Orgie [xh8Kuh2].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9268a65a-d371-42aa-8990-29ea3e192c0c.mp4" - }, - { - "id": "927117d5-0ad5-4c4e-9e2f-4c8ba8c3c9f7", - "created_date": "2024-07-25 07:29:45.930519", - "last_modified_date": "2024-07-25 07:29:45.930519", - "version": 0, - "url": "https://ge.xhamster.com/videos/die-versaute-nonne-full-movie-xhkdYAv", - "review": 0, - "should_download": 0, - "title": "Die Versaute Nonne Full Movie, Free Big Cock HD Porn 08 | xHamster", - "file_name": "Die Versaute Nonne (Full Movie) [xhkdYAv].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/927117d5-0ad5-4c4e-9e2f-4c8ba8c3c9f7.mp4" - }, - { - "id": "92732fa3-6030-4219-8bf0-5a07c7278925", - "created_date": "2024-12-29 23:53:27.961664", - "last_modified_date": "2024-12-29 23:53:27.961664", - "version": 0, - "url": "https://ge.xhamster.com/videos/erotic-adventure-1984-12373958", - "review": 0, - "should_download": 0, - "title": "Erotisches Abenteuer (1984) | xHamster", - "file_name": "Erotisches Abenteuer (1984) [12373958].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/92732fa3-6030-4219-8bf0-5a07c7278925.mp4" - }, - { - "id": "92a65421-b498-4e12-a73b-e41412933437", - "created_date": "2024-07-25 07:29:47.367911", - "last_modified_date": "2024-07-25 07:29:47.367911", - "version": 0, - "url": "https://ge.xhamster.com/videos/schoolgirls-geile-biester-auf-der-schulbank-1995-7720243", - "review": 0, - "should_download": 0, - "title": "Schoolgirls - Geile Biester Auf Der Schulbank 1995: Porn 8b | xHamster", - "file_name": "Schoolgirls - Geile Biester auf der Schulbank (1995) [7720243].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/92a65421-b498-4e12-a73b-e41412933437.mp4" - }, - { - "id": "92b09e8e-7946-42a5-aa0e-2fcc5cd44662", - "created_date": "2024-07-25 07:29:46.500238", - "last_modified_date": "2024-07-25 07:29:46.500238", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-best-friend-came-to-see-movies-but-i-fucked-her-xh4cWKj", - "review": 0, - "should_download": 0, - "title": "Meine beste freundin kam, um filme zu gucken, aber ich fickte sie | xHamster", - "file_name": "Meine beste freundin kam, um filme zu gucken, aber ich fickte sie [xh4cWKj].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/92b09e8e-7946-42a5-aa0e-2fcc5cd44662.mp4" - }, - { - "id": "92cc1c43-dfcc-400c-8566-e60254def2a6", - "created_date": "2024-07-25 07:29:46.015556", - "last_modified_date": "2024-07-25 07:29:46.015556", - "version": 0, - "url": "https://ge.xhamster.com/videos/vintage-hoppla-jetzt-komm-ich-xhmTm5V", - "review": 0, - "should_download": 0, - "title": "Vintage - Hoppla Jetzt Komm Ich, Free Porn cc | xHamster", - "file_name": "vintage - hoppla jetzt komm ich [xhmTm5V].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/92cc1c43-dfcc-400c-8566-e60254def2a6.mp4" - }, - { - "id": "93b4b18b-4dd3-4333-adf5-f5ece6fa3a65", - "created_date": "2024-07-25 07:29:46.311061", - "last_modified_date": "2024-07-25 07:29:46.311061", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-stepdad-and-his-stepbrother-fuck-my-holes-xh6z2tG", - "review": 0, - "should_download": 0, - "title": "Mein Stiefvater und sein Stiefbruder ficken meine L\u00f6cher | xHamster", - "file_name": "Mein Stiefvater und sein Stiefbruder ficken meine L\u00f6cher [xh6z2tG].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/93b4b18b-4dd3-4333-adf5-f5ece6fa3a65.mp4" - }, - { - "id": "93de10fa-b6e5-4e46-8196-0b84db62a937", - "created_date": "2024-07-25 07:29:44.247065", - "last_modified_date": "2024-07-25 07:29:44.247065", - "version": 0, - "url": "https://ge.xhamster.com/videos/bratty-sis-my-cock-slips-in-sisters-pussy-and-she-loves-it-8519881", - "review": 0, - "should_download": 0, - "title": "Bratty Sis - mein Schwanz rutscht in die Muschi der Schwester und sie liebt es | xHamster", - "file_name": "Bratty Sis - mein Schwanz rutscht in die Muschi der Schwester und sie liebt es [8519881].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/93de10fa-b6e5-4e46-8196-0b84db62a937.mp4" - }, - { - "id": "94001fb9-162e-4e55-a26f-71e44db34a19", - "created_date": "2024-09-11 10:23:29.176180", - "last_modified_date": "2024-10-21 16:30:45.132000", - "version": 1, - "url": "https://ge.xhamster.com/videos/stranger-cocks-caught-jerking-off-on-the-beach-and-juiced-xhihRQi", - "review": 0, - "should_download": 0, - "title": "Sch\u00f6nste Fremdschw\u00e4nze am Beach beim Wichsen erwischt und entsaftet | xHamster", - "file_name": "Sch\u00f6nste Fremdschw\u00e4nze am Beach beim Wichsen erwischt und entsaftet [xhihRQi].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/94001fb9-162e-4e55-a26f-71e44db34a19.mp4" - }, - { - "id": "9408796f-acd7-467b-924c-f605823326e1", - "created_date": "2024-07-25 07:29:44.857332", - "last_modified_date": "2024-07-25 07:29:44.857332", - "version": 0, - "url": "https://ge.xhamster.com/videos/american-taboo-3-xhHBJkZ", - "review": 0, - "should_download": 0, - "title": "Amerikanisches Tabu 3 | xHamster", - "file_name": "Amerikanisches Tabu 3 [xhHBJkZ].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9408796f-acd7-467b-924c-f605823326e1.mp4" - }, - { - "id": "944b6f42-4169-44fd-96cc-d10d7e5053cc", - "created_date": "2024-07-25 07:29:45.536342", - "last_modified_date": "2024-07-25 07:29:45.536342", - "version": 0, - "url": "https://ge.xhamster.com/videos/bi-in-der-schule-3858838", - "review": 0, - "should_download": 0, - "title": "Bi in Der Schule: Free Threesome Porn Video de | xHamster", - "file_name": "Bi in der Schule [3858838].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/944b6f42-4169-44fd-96cc-d10d7e5053cc.mp4" - }, - { - "id": "94857778-18c2-4061-a1c8-64a2fe923fd8", - "created_date": "2024-12-29 23:53:27.951571", - "last_modified_date": "2024-12-29 23:53:27.951571", - "version": 0, - "url": "https://ge.xhamster.com/videos/your-perverted-stepbrother-has-a-boner-xhrUsA8", - "review": 0, - "should_download": 0, - "title": "Dein perverser Stiefbruder hat eine Latte! | xHamster", - "file_name": "Dein perverser Stiefbruder hat eine Latte! [xhrUsA8].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/94857778-18c2-4061-a1c8-64a2fe923fd8.mp4" - }, - { - "id": "9489ef3c-e56e-4a54-8843-688d7e82aea9", - "created_date": "2024-12-29 23:53:27.933552", - "last_modified_date": "2024-12-29 23:53:27.933552", - "version": 0, - "url": "https://ge.xhamster.com/videos/bratty-sis-horny-bro-slips-cock-into-besties-teen-pussy-10303594", - "review": 0, - "should_download": 0, - "title": "Bratty Sis - ein geiler Bro rutscht Schwanz in die Teen-Muschi von Besties | xHamster", - "file_name": "Bratty Sis - ein geiler Bro rutscht Schwanz in die Teen-Muschi von Besties [10303594].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/9489ef3c-e56e-4a54-8843-688d7e82aea9.mp4" - }, - { - "id": "94a28ae0-a89f-453e-96c2-af2a5f54b61e", - "created_date": "2024-07-25 07:29:45.996882", - "last_modified_date": "2024-07-25 07:29:45.996882", - "version": 0, - "url": "https://ge.xhamster.com/videos/unerwartete-sexuelle-gelegenheiten-full-movie-xhzWrt0", - "review": 0, - "should_download": 0, - "title": "Unerwartete Sexuelle Gelegenheiten Full Movie: Free Porn 27 | xHamster", - "file_name": "Unerwartete sexuelle Gelegenheiten (Full Movie) [xhzWrt0].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/94a28ae0-a89f-453e-96c2-af2a5f54b61e.mp4" - }, - { - "id": "94a3b7ab-0862-4422-a326-46af2f1cf9a6", - "created_date": "2024-07-25 07:29:45.389999", - "last_modified_date": "2024-07-25 07:29:45.389999", - "version": 0, - "url": "https://ge.xhamster.com/videos/18-year-old-girl-gets-her-ass-deflowered-in-front-of-her-friend-xhkvWed", - "review": 0, - "should_download": 0, - "title": "18-J\u00e4hrige l\u00e4sst sich vor ihrer Freundin den Arsch entjungfern! | xHamster", - "file_name": "18-J\u00e4hrige l\u00e4sst sich vor ihrer Freundin den Arsch entjungfern! [xhkvWed].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/94a3b7ab-0862-4422-a326-46af2f1cf9a6.mp4" - }, - { - "id": "94b76c97-140c-4cf0-b256-c6ba9a022aee", - "created_date": "2024-07-25 07:29:47.532780", - "last_modified_date": "2024-07-25 07:29:47.532780", - "version": 0, - "url": "https://ge.xhamster.com/videos/american-college-xxx-vol-9-original-version-in-hd-xh81OaU", - "review": 0, - "should_download": 0, - "title": "American College xxx - vol (9) - (Originalversion in hd) | xHamster", - "file_name": "American College xxx - vol (9) - (Originalversion in hd) [xh81OaU].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/94b76c97-140c-4cf0-b256-c6ba9a022aee.mp4" - }, - { - "id": "94d4d56c-2a78-4e65-b7c8-3eb499f2329d", - "created_date": "2024-07-25 07:29:44.547159", - "last_modified_date": "2024-07-25 07:29:44.547159", - "version": 0, - "url": "https://ge.xhamster.com/videos/pure-orgy-1979-12159580", - "review": 0, - "should_download": 0, - "title": "Pure Orgie (1979) | xHamster", - "file_name": "Pure Orgie (1979) [12159580].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/94d4d56c-2a78-4e65-b7c8-3eb499f2329d.mp4" - }, - { - "id": "94f2fe48-6014-4d35-ba20-d8f5e736f7ee", - "created_date": "2024-07-25 07:29:45.571728", - "last_modified_date": "2024-07-25 07:29:45.571728", - "version": 0, - "url": "https://ge.xhamster.com/videos/die-munteren-sexspiele-unserer-nachbarn-1978-softcore-1630973", - "review": 0, - "should_download": 0, - "title": "Die Munteren Sexspiele Unserer Nachbarn 1978 Softcore | xHamster", - "file_name": "Die Munteren Sexspiele Unserer Nachbarn (1978) Softcore [1630973].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/94f2fe48-6014-4d35-ba20-d8f5e736f7ee.mp4" - }, - { - "id": "951cb63a-85f6-4c6b-a8c5-ae389ae4518d", - "created_date": "2024-12-29 23:53:27.896983", - "last_modified_date": "2024-12-29 23:53:27.896983", - "version": 0, - "url": "https://ge.xhamster.com/videos/hot-sauna-threesome-xhu1nDi", - "review": 0, - "should_download": 0, - "title": "Hei\u00dfer Sauna-Dreier | xHamster", - "file_name": "Hei\u00dfer Sauna-Dreier [xhu1nDi].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/951cb63a-85f6-4c6b-a8c5-ae389ae4518d.mp4" - }, - { - "id": "9527957a-ab3b-4273-a399-847a322d3532", - "created_date": "2024-07-25 07:29:48.021128", - "last_modified_date": "2024-07-25 07:29:48.021128", - "version": 0, - "url": "https://ge.xhamster.com/videos/summer-heat-1979-15008833", - "review": 0, - "should_download": 0, - "title": "Summer Heat (1979) | xHamster", - "file_name": "Summer Heat (1979) [15008833].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9527957a-ab3b-4273-a399-847a322d3532.mp4" - }, - { - "id": "9536377d-d3f6-4dee-ad12-b00666569775", - "created_date": "2024-07-25 07:29:46.489069", - "last_modified_date": "2024-07-25 07:29:46.489069", - "version": 0, - "url": "https://ge.xhamster.com/videos/i-saw-my-stepmom-masturbating-s15-e6-xhWZQgd", - "review": 0, - "should_download": 0, - "title": "Ich habe meine stiefmutter masturbiert - s15: e6 | xHamster", - "file_name": "Ich habe meine stiefmutter masturbiert - s15\uff1a e6 [xhWZQgd].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9536377d-d3f6-4dee-ad12-b00666569775.mp4" - }, - { - "id": "9556c2b8-a2b2-42a9-83ca-0f1f6d8bbba7", - "created_date": "2024-07-25 07:29:45.128161", - "last_modified_date": "2024-07-25 07:29:45.128161", - "version": 0, - "url": "https://ge.xhamster.com/videos/vintage-anal-full-movie-01-little-french-maid-a85-xhxZ8SI", - "review": 0, - "should_download": 0, - "title": "Retro anal, kompletter Film 01 (kleines franz\u00f6sisches Zimmerm\u00e4dchen) - a85 | xHamster", - "file_name": "Retro anal, kompletter Film 01 (kleines franz\u00f6sisches Zimmerm\u00e4dchen) - a85 [xhxZ8SI].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/9556c2b8-a2b2-42a9-83ca-0f1f6d8bbba7.mp4" - }, - { - "id": "95702616-a5a0-411d-a82f-59cd45bff466", - "created_date": "2024-07-25 07:29:47.676118", - "last_modified_date": "2024-07-25 07:29:47.676118", - "version": 0, - "url": "https://ge.xhamster.com/videos/games-with-stepsis-xhbMCjr", - "review": 0, - "should_download": 0, - "title": "Spiele mit Stiefschwester | xHamster", - "file_name": "Spiele mit Stiefschwester [xhbMCjr].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/95702616-a5a0-411d-a82f-59cd45bff466.mp4" - }, - { - "id": "957b5397-fc0e-4f50-a6f9-fe71f9388462", - "created_date": "2024-07-25 07:29:44.238523", - "last_modified_date": "2024-07-25 07:29:44.238523", - "version": 0, - "url": "https://ge.xhamster.com/videos/kinky-family-lacy-lennon-my-stepsis-took-my-virginity-11178868", - "review": 0, - "should_download": 0, - "title": "Versaute Familie - Lacy Lennon - meine Stiefschwester hat meine Jungfr\u00e4ulichkeit genommen | xHamster", - "file_name": "Versaute Familie - Lacy Lennon - meine Stiefschwester hat meine Jungfr\u00e4ulichkeit genommen [11178868].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/957b5397-fc0e-4f50-a6f9-fe71f9388462.mp4" - }, - { - "id": "958ec084-5033-441d-af65-9e10ed9729f1", - "created_date": "2024-07-25 07:29:46.403732", - "last_modified_date": "2024-07-25 07:29:46.403732", - "version": 0, - "url": "https://ge.xhamster.com/videos/you-should-spend-more-time-outdoors-8-6801304", - "review": 0, - "should_download": 0, - "title": "Sie sollten mehr Zeit im Freien verbringen # 8 | xHamster", - "file_name": "Sie sollten mehr Zeit im Freien verbringen # 8 [6801304].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/958ec084-5033-441d-af65-9e10ed9729f1.mp4" - }, - { - "id": "95d3690f-3acb-4b8a-9aa3-e8c9993bcddd", - "created_date": "2024-07-25 07:29:46.469630", - "last_modified_date": "2024-07-25 07:29:46.469630", - "version": 0, - "url": "https://ge.xhamster.com/videos/bratty-sis-being-extra-nice-to-her-step-brother-s7-e9-10922908", - "review": 0, - "should_download": 0, - "title": "Bratty sis - besonders nett zu ihrem Stiefbruder s7: e9 sein | xHamster", - "file_name": "Bratty sis - besonders nett zu ihrem Stiefbruder s7\uff1a e9 sein [10922908].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/95d3690f-3acb-4b8a-9aa3-e8c9993bcddd.mp4" - }, - { - "id": "95f37065-f536-4e60-bfb5-1411fc3f1599", - "created_date": "2024-07-25 07:29:44.499198", - "last_modified_date": "2024-07-25 07:29:44.499198", - "version": 0, - "url": "https://ge.xhamster.com/videos/pretty-woman-asked-to-smear-her-back-with-cream-xhERPxV", - "review": 0, - "should_download": 0, - "title": "H\u00fcbsche frau bat, sie mit lotion zu reiben | xHamster", - "file_name": "H\u00fcbsche frau bat, sie mit lotion zu reiben [xhERPxV].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/95f37065-f536-4e60-bfb5-1411fc3f1599.mp4" - }, - { - "id": "95f89afd-d77a-455b-b5a9-9f4d9c6cb5e0", - "created_date": "2024-07-25 07:29:47.762798", - "last_modified_date": "2024-07-25 07:29:47.762798", - "version": 0, - "url": "https://ge.xhamster.com/videos/swapdaughter-molly-little-shows-milf-katrina-colt-the-perks-of-being-freeuse-s7-e5-xhemlnp", - "review": 0, - "should_download": 0, - "title": "Swapdaughter Molly Little zeigt MILF Katrina Colt die perks, \"Freeuse\" zu sein - S7: E5 | xHamster", - "file_name": "Swapdaughter Molly Little zeigt MILF Katrina Colt die perks, \uff02Freeuse\uff02 zu sein - S7\uff1a E5 [xhemlnp].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/95f89afd-d77a-455b-b5a9-9f4d9c6cb5e0.mp4" - }, - { - "id": "9623298e-4b60-4870-a1eb-bec06fb97e02", - "created_date": "2024-09-24 08:11:38.999255", - "last_modified_date": "2024-10-21 16:30:59.458000", - "version": 1, - "url": "https://ge.xhamster.com/videos/pure-taboo-scheming-boyfriend-wants-to-see-his-gfs-stepmom-sarah-vandella-make-her-orgasm-xh4dYTP", - "review": 0, - "should_download": 0, - "title": "Pure Tabu, intriganter Freund will sehen, wie die Stiefmutter seiner Freundin Sarah Vandella ihren Orgasmus macht | xHamster", - "file_name": "Pure Tabu, intriganter Freund will sehen, wie die Stiefmutter seiner Freundin Sarah Vandella ihren Orgasmus macht [xh4dYTP].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/9623298e-4b60-4870-a1eb-bec06fb97e02.mp4" - }, - { - "id": "966848c9-20ec-4fb3-8902-8e5523fcd0d6", - "created_date": "2024-07-25 07:29:46.926120", - "last_modified_date": "2024-07-25 07:29:46.926120", - "version": 0, - "url": "https://ge.xhamster.com/videos/familie-brunzbichler-3-3156748", - "review": 0, - "should_download": 0, - "title": "Familie Brunzbichler 3, Free Hardcore Porn 52 | xHamster", - "file_name": "Familie Brunzbichler 3 [3156748].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/966848c9-20ec-4fb3-8902-8e5523fcd0d6.mp4" - }, - { - "id": "96a63276-f917-4bea-ad28-fc1f95ef3eb1", - "created_date": "2024-07-25 07:29:46.053259", - "last_modified_date": "2024-07-25 07:29:46.053259", - "version": 0, - "url": "https://ge.xhamster.com/videos/a-super-busty-german-brunette-gets-her-muff-hammered-outdoors-xhBBjTp", - "review": 0, - "should_download": 0, - "title": "Eine super vollbusige deutsche Br\u00fcnette bekommt drau\u00dfen ihre Muschi geh\u00e4mmert | xHamster", - "file_name": "Eine super vollbusige deutsche Br\u00fcnette bekommt drau\u00dfen ihre Muschi geh\u00e4mmert [xhBBjTp].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/96a63276-f917-4bea-ad28-fc1f95ef3eb1.mp4" - }, - { - "id": "970f6be9-f5dc-4b0f-b6cd-2348c8e77661", - "created_date": "2024-07-25 07:29:46.324252", - "last_modified_date": "2024-07-25 07:29:46.324252", - "version": 0, - "url": "https://ge.xhamster.com/videos/pov-of-the-wife-blowing-me-in-the-creek-xhLwTKD", - "review": 0, - "should_download": 0, - "title": "POV der ehefrau bl\u00e4st mich im bach | xHamster", - "file_name": "POV der ehefrau bl\u00e4st mich im bach [xhLwTKD].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/970f6be9-f5dc-4b0f-b6cd-2348c8e77661.mp4" - }, - { - "id": "971d23d0-72e9-4173-ad70-aa01394cff8b", - "created_date": "2024-07-25 07:29:47.175568", - "last_modified_date": "2024-07-25 07:29:47.175568", - "version": 0, - "url": "https://ge.xhamster.com/videos/hot-teacher-tricks-students-into-threeway-fuck-7032708", - "review": 0, - "should_download": 0, - "title": "Hei\u00dfe Lehrerin trickst Sch\u00fcler in Dreier-Fick aus | xHamster", - "file_name": "Hei\u00dfe Lehrerin trickst Sch\u00fcler in Dreier-Fick aus [7032708].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/971d23d0-72e9-4173-ad70-aa01394cff8b.mp4" - }, - { - "id": "97512386-f969-4d0b-a0ed-c9cce7af3b6a", - "created_date": "2024-07-25 07:29:44.729616", - "last_modified_date": "2024-07-25 07:29:44.729616", - "version": 0, - "url": "https://ge.xhamster.com/videos/secret-vacation-xhFh3Nq", - "review": 0, - "should_download": 0, - "title": "Geheime Ferien | xHamster", - "file_name": "Geheime Ferien [xhFh3Nq].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/97512386-f969-4d0b-a0ed-c9cce7af3b6a.mp4" - }, - { - "id": "9777c0c1-dec4-4d43-a707-8ced8cf3d5e7", - "created_date": "2024-07-25 07:29:44.314917", - "last_modified_date": "2024-07-25 07:29:44.314917", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-friend-was-coming-to-the-solarium-at-the-club-and-ended-up-having-sex-with-me-and-my-friends-at-the-pool-xhKLBoG", - "review": 0, - "should_download": 0, - "title": "Mein Freund kam ins Solarium des Clubs und hatte am Ende Sex mit mir und meinen Freunden am Pool | xHamster", - "file_name": "Mein Freund kam ins Solarium des Clubs und hatte am Ende Sex mit mir und meinen Freunden am Pool [xhKLBoG].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/9777c0c1-dec4-4d43-a707-8ced8cf3d5e7.mp4" - }, - { - "id": "97bce4aa-9306-4fd5-b3a5-eafaf8daedd8", - "created_date": "2024-07-25 07:29:46.416644", - "last_modified_date": "2024-07-25 07:29:46.416644", - "version": 0, - "url": "https://ge.xhamster.com/videos/step-brother-cum-twice-with-his-new-step-sister-xhWUdlD", - "review": 0, - "should_download": 0, - "title": "Stiefbruder kommt zweimal mit seiner neuen Stiefschwester | xHamster", - "file_name": "Stiefbruder kommt zweimal mit seiner neuen Stiefschwester [xhWUdlD].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/97bce4aa-9306-4fd5-b3a5-eafaf8daedd8.mp4" - }, - { - "id": "97c2da74-f79a-4af6-a20c-7bf7a74dfa62", - "created_date": "2024-07-25 07:29:46.281847", - "last_modified_date": "2024-07-25 07:29:46.281847", - "version": 0, - "url": "https://ge.xhamster.com/videos/nachsitzen-nr-1-full-movie-xhGgGzw", - "review": 0, - "should_download": 0, - "title": "Nachsitzen Nr 1 Full Movie, Free Big Cock Porn ff | xHamster", - "file_name": "Nachsitzen Nr.1 (Full Movie) [xhGgGzw].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/97c2da74-f79a-4af6-a20c-7bf7a74dfa62.mp4" - }, - { - "id": "97c5c7e5-77a3-4822-a47f-a9f9437202fd", - "created_date": "2024-07-25 07:29:44.566165", - "last_modified_date": "2024-07-25 07:29:44.566165", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsis-says-your-sperm-are-probably-so-little-you-wouldnt-even-have-a-chance-of-getting-me-pregnant-xhN7r0m", - "review": 0, - "should_download": 0, - "title": "Stiefschwester sagt, dein Sperma ist wahrscheinlich so klein, dass du keine Chance h\u00e4ttest, mich schwanger zu machen | xHamster", - "file_name": "Stiefschwester sagt, dein Sperma ist wahrscheinlich so klein, dass du keine Chance h\u00e4ttest, mich schwanger zu machen [xhN7r0m].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/97c5c7e5-77a3-4822-a47f-a9f9437202fd.mp4" - }, - { - "id": "97dbcc38-cb27-4efb-ad92-02871687a1e2", - "created_date": "2024-07-25 07:29:44.251056", - "last_modified_date": "2024-07-25 07:29:44.251056", - "version": 0, - "url": "https://ge.xhamster.com/videos/teen-vanilla-skye-gets-horny-in-the-pool-and-loves-contorting-to-suck-dick-xhBFNAu", - "review": 0, - "should_download": 0, - "title": "Teenie Vanilla Skye wird im Pool geil und liebt es, sich zu lutschen, um Schwanz zu lutschen | xHamster", - "file_name": "Teenie Vanilla Skye wird im Pool geil und liebt es, sich zu lutschen, um Schwanz zu lutschen [xhBFNAu].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/97dbcc38-cb27-4efb-ad92-02871687a1e2.mp4" - }, - { - "id": "985687ee-d6c7-4748-a610-e75f75029673", - "created_date": "2024-07-25 07:29:44.590039", - "last_modified_date": "2024-07-25 07:29:44.590039", - "version": 0, - "url": "https://ge.xhamster.com/videos/at-the-office-come-fuck-my-wet-horny-pussy-and-splash-me-with-your-juice-xhhJ3Da", - "review": 0, - "should_download": 0, - "title": "Im B\u00fcro: Komm, fick meine nasse, geile Muschi und spritz mich mit deinem Saft | xHamster", - "file_name": "Im B\u00fcro\uff1a Komm, fick meine nasse, geile Muschi und spritz mich mit deinem Saft [xhhJ3Da].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/985687ee-d6c7-4748-a610-e75f75029673.mp4" - }, - { - "id": "9876e83a-e7b8-45d3-a200-be16a1a8c4a1", - "created_date": "2024-07-25 07:29:45.529325", - "last_modified_date": "2024-07-25 07:29:45.529325", - "version": 0, - "url": "https://ge.xhamster.com/videos/german-amateur-vintage-complete-film-b-r-11312553", - "review": 0, - "should_download": 0, - "title": "Deutscher Amateur-Retro - kompletter Film -b $ r | xHamster", - "file_name": "Deutscher Amateur-Retro - kompletter Film -b $ r [11312553].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9876e83a-e7b8-45d3-a200-be16a1a8c4a1.mp4" - }, - { - "id": "9882123d-539a-4a03-9c59-d3a98ecbcc39", - "created_date": "2024-07-25 07:29:45.567222", - "last_modified_date": "2024-07-25 07:29:45.567222", - "version": 0, - "url": "https://ge.xhamster.com/videos/lexi-luna-plays-with-chloe-surreal-before-sharing-her-bfs-dick-with-her-stepdaughter-leana-lovings-brazzers-xhC23iG", - "review": 0, - "should_download": 0, - "title": "Lexi Luna spielt mit Chloe Surreal, bevor sie den Schwanz ihres Freundes mit ihrer Stieftochter Leana Lovings - Brazzers - teilt | xHamster", - "file_name": "Lexi Luna spielt mit Chloe Surreal, bevor sie den Schwanz ihres Freundes mit ihrer Stieftochter Leana Lovings - Brazzers - teilt [xhC23iG].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9882123d-539a-4a03-9c59-d3a98ecbcc39.mp4" - }, - { - "id": "98859d43-9ce0-4a7f-9e36-2a975597cbab", - "created_date": "2024-07-25 07:29:44.752928", - "last_modified_date": "2024-07-25 07:29:44.752928", - "version": 0, - "url": "https://ge.xhamster.com/videos/after-a-night-out-at-the-disco-we-hooked-up-with-this-big-ass-xhFuTro", - "review": 0, - "should_download": 0, - "title": "Nach einer nacht in der disco haben wir es mit diesem dicken arsch gefickt | xHamster", - "file_name": "Nach einer nacht in der disco haben wir es mit diesem dicken arsch gefickt [xhFuTro].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/98859d43-9ce0-4a7f-9e36-2a975597cbab.mp4" - }, - { - "id": "98c8dd69-26f4-4317-9a35-ed6b43e9bc5e", - "created_date": "2024-09-24 08:11:39.006580", - "last_modified_date": "2024-10-21 16:31:04.988000", - "version": 1, - "url": "https://ge.xhamster.com/videos/sister-teach-brother-a-lesson-for-peeping-12908263", - "review": 0, - "should_download": 0, - "title": "Schwester lehrt Bruder eine Lektion f\u00fcr das Gucken | xHamster", - "file_name": "Schwester lehrt Bruder eine Lektion f\u00fcr das Gucken [12908263].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/98c8dd69-26f4-4317-9a35-ed6b43e9bc5e.mp4" - }, - { - "id": "98c958ea-ff30-4ca8-81c6-f92f3bf64b28", - "created_date": "2024-12-29 23:53:27.936731", - "last_modified_date": "2024-12-29 23:53:27.936731", - "version": 0, - "url": "https://ge.xhamster.com/videos/step-brother-step-sister-share-a-hotel-room-rhaya-shyne-teamskeet-classics-xhe9fqf", - "review": 0, - "should_download": 0, - "title": "Stiefbruder und Stiefschwester teilen sich ein Hotelzimmer - rhaya shyne - Klassiker im Teamskeet | xHamster", - "file_name": "Stiefbruder und Stiefschwester teilen sich ein Hotelzimmer - rhaya shyne - Klassiker im Teamskeet [xhe9fqf].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/98c958ea-ff30-4ca8-81c6-f92f3bf64b28.mp4" - }, - { - "id": "9928b4a6-130d-4c64-930d-a5b1c8b60afc", - "created_date": "2024-07-25 07:29:46.234992", - "last_modified_date": "2024-07-25 07:29:46.234992", - "version": 0, - "url": "https://ge.xhamster.com/videos/secretary-fucks-boss-waiting-for-her-husband-to-pick-her-up-xhO6alz", - "review": 0, - "should_download": 0, - "title": "Sekret\u00e4rin fickt Chef und wartet darauf, dass ihr Ehemann sie abholt | xHamster", - "file_name": "Sekret\u00e4rin fickt Chef und wartet darauf, dass ihr Ehemann sie abholt [xhO6alz].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9928b4a6-130d-4c64-930d-a5b1c8b60afc.mp4" - }, - { - "id": "998424c2-3a19-486c-a617-2cb19c34975e", - "created_date": "2024-07-25 07:29:47.254496", - "last_modified_date": "2024-07-25 07:29:47.254496", - "version": 0, - "url": "https://ge.xhamster.com/videos/dont-worry-he-is-my-stepdaddy-vol-10-xhia72d", - "review": 0, - "should_download": 0, - "title": "Keine Sorge, er ist mein Stiefvater !!! - Vol # 10 | xHamster", - "file_name": "Keine Sorge, er ist mein Stiefvater !!! - Vol # 10 [xhia72d].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/998424c2-3a19-486c-a617-2cb19c34975e.mp4" - }, - { - "id": "99b87cb8-2f13-4b3e-b33d-116938b28eaa", - "created_date": "2024-07-25 07:29:44.764263", - "last_modified_date": "2024-07-25 07:29:44.764263", - "version": 0, - "url": "https://ge.xhamster.com/videos/die-beichte-der-josefinemutzenbacher-1979-5841900", - "review": 0, - "should_download": 0, - "title": "Die Beichte Der Josefinemutzenbacher 1979: Free Porn c7 | xHamster", - "file_name": "Die Beichte der JosefineMutzenbacher 1979 [5841900].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/99b87cb8-2f13-4b3e-b33d-116938b28eaa.mp4" - }, - { - "id": "99cfeb34-dd02-4b95-9080-903c31a65a54", - "created_date": "2024-07-25 07:29:44.515484", - "last_modified_date": "2024-07-25 07:29:44.515484", - "version": 0, - "url": "https://ge.xhamster.com/videos/versaute-familie-xhaTScH", - "review": 0, - "should_download": 0, - "title": "Versaute Familie: Free European Porn Video aa | xHamster", - "file_name": "Versaute Familie [xhaTScH].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/99cfeb34-dd02-4b95-9080-903c31a65a54.mp4" - }, - { - "id": "99e6bf98-7d7d-4af7-a43f-724a01723b66", - "created_date": "2024-07-25 07:29:45.174370", - "last_modified_date": "2024-07-25 07:29:45.174370", - "version": 0, - "url": "https://ge.xhamster.com/videos/ein-total-versautes-buro-3261734", - "review": 0, - "should_download": 0, - "title": "Ein Total Versautes Buro, Free Retro Porn 8f | xHamster", - "file_name": "Ein Total Versautes Buro [3261734].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/99e6bf98-7d7d-4af7-a43f-724a01723b66.mp4" - }, - { - "id": "99fb9b0d-c63e-4433-be56-5030ebe25ac2", - "created_date": "2024-07-25 07:29:47.167182", - "last_modified_date": "2024-07-25 07:29:47.167182", - "version": 0, - "url": "https://ge.xhamster.com/videos/neighborhood-doctor-punishment-3661901", - "review": 0, - "should_download": 0, - "title": "Nachbarschaftsarzt-Bestrafung | xHamster", - "file_name": "Nachbarschaftsarzt-Bestrafung [3661901].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/99fb9b0d-c63e-4433-be56-5030ebe25ac2.mp4" - }, - { - "id": "9a002058-a3b7-411a-88c2-00c38f136211", - "created_date": "2024-07-25 07:29:47.696579", - "last_modified_date": "2024-07-25 07:29:47.696579", - "version": 0, - "url": "https://ge.xhamster.com/videos/junge-knospen-budding-beauties-1991-xhCVAoW", - "review": 0, - "should_download": 0, - "title": "Junge Knospen - Budding Beauties 1991, Porn f1 | xHamster", - "file_name": "Junge Knospen - Budding Beauties 1991 [xhCVAoW].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9a002058-a3b7-411a-88c2-00c38f136211.mp4" - }, - { - "id": "9a02e41f-6def-406b-9bf5-f35dd154d7af", - "created_date": "2024-07-25 07:29:47.635929", - "last_modified_date": "2024-07-25 07:29:47.635929", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsiblings-stepbro-helps-sis-shave-and-licked-in-her-pus-9478938", - "review": 0, - "should_download": 0, - "title": "Stiefschwester - Stiefbruder hilft ihrer Schwester, sich zu rasieren und ihren Eiter zu lecken | xHamster", - "file_name": "Stiefschwester - Stiefbruder hilft ihrer Schwester, sich zu rasieren und ihren Eiter zu lecken [9478938].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9a02e41f-6def-406b-9bf5-f35dd154d7af.mp4" - }, - { - "id": "9abf8d88-1b41-47d5-a88f-a4c10ecd8523", - "created_date": "2024-07-25 07:29:45.858444", - "last_modified_date": "2024-07-25 07:29:45.858444", - "version": 0, - "url": "https://ge.xhamster.com/videos/marilyn-jess-1-german-vintage-compilation-70s-80s-2092799", - "review": 0, - "should_download": 0, - "title": "Marilyn Jess 1 deutsche Retro-Zusammenstellung der 70er und 80er Jahre | xHamster", - "file_name": "Marilyn Jess 1 deutsche Retro-Zusammenstellung der 70er und 80er Jahre [2092799].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9abf8d88-1b41-47d5-a88f-a4c10ecd8523.mp4" - }, - { - "id": "9b64eeff-4864-4a9a-8b29-1c047ae466f0", - "created_date": "2024-07-25 07:29:47.809360", - "last_modified_date": "2024-07-25 07:29:47.809360", - "version": 0, - "url": "https://ge.xhamster.com/videos/auf-hoher-see-full-german-movie-xhxTkgk", - "review": 0, - "should_download": 0, - "title": "Auf Hoher See- Full German Movie, Free Porn 9d | xHamster", - "file_name": "Auf Hoher See- full german movie [xhxTkgk].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9b64eeff-4864-4a9a-8b29-1c047ae466f0.mp4" - }, - { - "id": "9b7a30d9-b0ca-489b-83ce-8c955eababe3", - "created_date": "2024-07-25 07:29:44.617073", - "last_modified_date": "2024-07-25 07:29:44.617073", - "version": 0, - "url": "https://ge.xhamster.com/videos/sperma-schluckspechte-1990-xhdN6uc", - "review": 0, - "should_download": 0, - "title": "Sperma Schluckspechte 1990, Free European Porn bf | xHamster", - "file_name": "Sperma Schluckspechte (1990) [xhdN6uc].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/9b7a30d9-b0ca-489b-83ce-8c955eababe3.mp4" - }, - { - "id": "9b8058ec-9305-46b6-bc6f-6b928a6f0212", - "created_date": "2024-07-25 07:29:46.866901", - "last_modified_date": "2024-07-25 07:29:46.866901", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-friends-hot-mom-is-alyssa-bruce-xhhEp44", - "review": 0, - "should_download": 0, - "title": "Die hei\u00dfe mutter meines freundes ist Alyssa bruce | xHamster", - "file_name": "Die hei\u00dfe mutter meines freundes ist Alyssa bruce [xhhEp44].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9b8058ec-9305-46b6-bc6f-6b928a6f0212.mp4" - }, - { - "id": "9bec3afd-8011-4289-813e-e2a538044c2b", - "created_date": "2024-07-25 07:29:44.772331", - "last_modified_date": "2024-07-25 07:29:44.772331", - "version": 0, - "url": "https://ge.xhamster.com/videos/sextsunami-84-10676705", - "review": 0, - "should_download": 0, - "title": "Sextsunami 84: Free HD Porn Video 11 | xHamster", - "file_name": "sextsunami 84 [10676705].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9bec3afd-8011-4289-813e-e2a538044c2b.mp4" - }, - { - "id": "9c011916-bc0d-41bf-ba12-651bb4f657fa", - "created_date": "2024-07-25 07:29:45.633085", - "last_modified_date": "2024-07-25 07:29:45.633085", - "version": 0, - "url": "https://ge.xhamster.com/videos/wild-holidays-full-movie-xhtJfKB", - "review": 0, - "should_download": 0, - "title": "Wilde Feiertage (kompletter Film) | xHamster", - "file_name": "Wilde Feiertage (kompletter Film) [xhtJfKB].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9c011916-bc0d-41bf-ba12-651bb4f657fa.mp4" - }, - { - "id": "9c1b1beb-26ff-4ad0-b5bb-59231a0556ca", - "created_date": "2024-07-25 07:29:47.600783", - "last_modified_date": "2024-07-25 07:29:47.600783", - "version": 0, - "url": "https://ge.xhamster.com/videos/evhb-14847939", - "review": 0, - "should_download": 0, - "title": "Evhb | xHamster", - "file_name": "Evhb [14847939].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9c1b1beb-26ff-4ad0-b5bb-59231a0556ca.mp4" - }, - { - "id": "9cf124fa-323b-475b-91f8-b3c698696f1d", - "created_date": "2024-07-25 07:29:44.519056", - "last_modified_date": "2024-07-25 07:29:44.519056", - "version": 0, - "url": "https://ge.xhamster.com/videos/hot-wife-cory-chase-shared-and-dpd-after-gym-workout-xh0nbi1", - "review": 0, - "should_download": 0, - "title": "Hei\u00dfe Ehefrau Cory Chase geteilt und nach dem Fitnesstraining doppelpenetriert | xHamster", - "file_name": "Hei\u00dfe Ehefrau Cory Chase geteilt und nach dem Fitnesstraining doppelpenetriert [xh0nbi1].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9cf124fa-323b-475b-91f8-b3c698696f1d.mp4" - }, - { - "id": "9d60df03-6dfa-4683-a624-10fe6162dcae", - "created_date": "2024-07-25 07:29:47.752365", - "last_modified_date": "2024-09-06 09:41:31.078000", - "version": 1, - "url": "https://ge.xhamster.com/videos/taboo-1980-full-vintage-movie-14390985", - "review": 0, - "should_download": 0, - "title": "Tabu (1980 kompletter Retro-Film)", - "file_name": "Tabu (1980 kompletter Retro-Film) [14390985].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9d60df03-6dfa-4683-a624-10fe6162dcae.mp4" - }, - { - "id": "9dd578ba-a812-4b96-ada6-a43be8e681e9", - "created_date": "2024-07-25 07:29:45.229523", - "last_modified_date": "2024-07-25 07:29:45.229523", - "version": 0, - "url": "https://ge.xhamster.com/videos/little-darlings-retro-13590759", - "review": 0, - "should_download": 0, - "title": "Kleine Lieblinge Retro | xHamster", - "file_name": "Kleine Lieblinge Retro [13590759].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/9dd578ba-a812-4b96-ada6-a43be8e681e9.mp4" - }, - { - "id": "9df9a129-3eb0-4c6d-8d9a-5ece1bbb29af", - "created_date": "2024-07-25 07:29:47.457926", - "last_modified_date": "2024-07-25 07:29:47.457926", - "version": 0, - "url": "https://ge.xhamster.com/videos/roommate-didnt-know-i-was-back-home-xhr7DMI", - "review": 0, - "should_download": 0, - "title": "Mitbewohner wusste nicht, dass ich zu Hause war | xHamster", - "file_name": "Mitbewohner wusste nicht, dass ich zu Hause war [xhr7DMI].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9df9a129-3eb0-4c6d-8d9a-5ece1bbb29af.mp4" - }, - { - "id": "9e59cbb6-5b26-455d-aa58-6582561c77a6", - "created_date": "2024-08-08 00:54:26.820708", - "last_modified_date": "2024-08-16 11:10:41.816000", - "version": 1, - "url": "https://ge.xhamster.com/videos/fakeagentuk-huge-facial-for-hot-petite-librarian-at-casting-4781377", - "review": 0, - "should_download": 0, - "title": "Fakeagentuk Huge Facial for Hot Petite Librarian at Casting | xHamster", - "file_name": "FakeAgentUK Huge facial for hot petite librarian at casting-4781377.mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9e59cbb6-5b26-455d-aa58-6582561c77a6.mp4" - }, - { - "id": "9e822efc-11a7-416c-84c9-986ea236d0fe", - "created_date": "2024-08-28 23:21:54.374622", - "last_modified_date": "2024-08-28 23:21:54.374622", - "version": 0, - "url": "https://ge.xhamster.com/videos/joi-fr-my-roommate-catch-me-watching-porn-i-tell-him-to-jerk-off-in-front-of-me-xhfk1Zp", - "review": 0, - "should_download": 0, - "title": "Joi fr - mein mitbewohner erwischt mich beim porno, ich sage ihm, er soll vor mir wichsen | xHamster", - "file_name": "Joi fr - mein mitbewohner erwischt mich beim porno, ich sage ihm, er soll vor mir wichsen [xhfk1Zp].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/9e822efc-11a7-416c-84c9-986ea236d0fe.mp4" - }, - { - "id": "9e8884bc-decb-4422-85c8-86a397f29d0d", - "created_date": "2024-07-25 07:29:46.546529", - "last_modified_date": "2025-01-03 11:56:30.438000", - "version": 1, - "url": "https://ge.xhamster.com/videos/quiet-quiet-youll-wake-my-stepmom-hotel-guest-fucked-the-beautiful-hostess-and-her-cute-stepdaughter-xhWHS4j", - "review": 0, - "should_download": 0, - "title": "Ruhig, ruhig! Du wirst meine stiefmutter wecken! Hotelgast fickte die sch\u00f6ne gastgeberin und ihre s\u00fc\u00dfe stieftochter! | xHamster", - "file_name": "Ruhig, ruhig! Du wirst meine stiefmutter wecken! Hotelgast fickte die sch\u00f6ne gastgeberin und ihre s\u00fc\u00dfe stieftochter! [xhWHS4j].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9e8884bc-decb-4422-85c8-86a397f29d0d.mp4" - }, - { - "id": "9e8c56d1-15f8-4bd6-809e-38537e0d1c8e", - "created_date": "2024-07-25 07:29:46.493075", - "last_modified_date": "2024-07-25 07:29:46.493075", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-golden-age-of-danish-porno-1970-1974-9614343", - "review": 0, - "should_download": 0, - "title": "Das goldene Zeitalter des d\u00e4nischen Pornos 1970-1974 | xHamster", - "file_name": "Das goldene Zeitalter des d\u00e4nischen Pornos 1970-1974 [9614343].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9e8c56d1-15f8-4bd6-809e-38537e0d1c8e.mp4" - }, - { - "id": "9ea37128-7f93-4020-963b-193e3300ce6b", - "created_date": "2024-07-25 07:29:45.281636", - "last_modified_date": "2024-07-25 07:29:45.281636", - "version": 0, - "url": "https://ge.xhamster.com/videos/strange-family-1977-13694110", - "review": 0, - "should_download": 0, - "title": "Strange Family (1977) | xHamster", - "file_name": "Strange Family (1977) [13694110].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9ea37128-7f93-4020-963b-193e3300ce6b.mp4" - }, - { - "id": "9f030602-ef27-4b27-bf0b-223e80a5f8c9", - "created_date": "2024-12-29 23:53:27.956712", - "last_modified_date": "2024-12-29 23:53:27.956712", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-seduction-of-cindy-xhlnaIY", - "review": 0, - "should_download": 0, - "title": "Die Verf\u00fchrung von Cindy | xHamster", - "file_name": "Die Verf\u00fchrung von Cindy [xhlnaIY].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/9f030602-ef27-4b27-bf0b-223e80a5f8c9.mp4" - }, - { - "id": "9fc7db28-17bd-49f7-a896-c8b79251b45c", - "created_date": "2024-07-25 07:29:47.043835", - "last_modified_date": "2024-07-25 07:29:47.043835", - "version": 0, - "url": "https://ge.xhamster.com/videos/sexy-boat-full-movie-xhbet4F", - "review": 0, - "should_download": 0, - "title": "Sexy Boot (kompletter Film) | xHamster", - "file_name": "Sexy Boot (kompletter Film) [xhbet4F].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/9fc7db28-17bd-49f7-a896-c8b79251b45c.mp4" - }, - { - "id": "9fd234ee-3cad-4bdf-974d-527f49359930", - "created_date": "2024-07-25 07:29:45.552726", - "last_modified_date": "2024-07-25 07:29:45.552726", - "version": 0, - "url": "https://ge.xhamster.com/videos/mrs-sanders-wants-her-sexy-babysitter-to-fuck-her-husband-xhDMPwY", - "review": 0, - "should_download": 0, - "title": "Mrs Sanders will, dass ihr sexy Babysitter ihren Ehemann fickt | xHamster", - "file_name": "Mrs Sanders will, dass ihr sexy Babysitter ihren Ehemann fickt [xhDMPwY].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/9fd234ee-3cad-4bdf-974d-527f49359930.mp4" - }, - { - "id": "a0027f8d-c07c-460a-b6fd-4196641792fb", - "created_date": "2025-01-19 13:42:32.817625", - "last_modified_date": "2025-01-19 13:42:32.817632", - "version": 0, - "url": "https://ge.xhamster.com/videos/our-horny-step-brother-wouldnt-stop-bothering-us-so-we-let-him-fuck-us-xh6z62K", - "review": 0, - "should_download": 0, - "title": "Unser geiler stiefbruder w\u00fcrde nicht aufh\u00f6ren, uns zu bel\u00e4stigen, also haben wir ihn uns ficken lassen | xHamster", - "file_name": "Unser geiler stiefbruder w\u00fcrde nicht aufh\u00f6ren, uns zu bel\u00e4stigen, also haben wir ihn uns ficken lassen [xh6z62K].mp4", - "path": null, - "cloud_link": null - }, - { - "id": "a0294259-12bc-4610-9913-eb8e366e8e8b", - "created_date": "2024-07-25 07:29:46.519061", - "last_modified_date": "2024-07-25 07:29:46.519061", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsons-overactive-sex-drive-brianna-beach-xh2F1Bu", - "review": 0, - "should_download": 0, - "title": "Stiefsohns \u00fcberaktiver Sexualtrieb - Brianna Beach | xHamster", - "file_name": "Stiefsohns \u00fcberaktiver Sexualtrieb - Brianna Beach [xh2F1Bu].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a0294259-12bc-4610-9913-eb8e366e8e8b.mp4" - }, - { - "id": "a096b607-b352-44d0-afda-26ee0daaf42e", - "created_date": "2024-07-25 07:29:47.851038", - "last_modified_date": "2024-07-25 07:29:47.851038", - "version": 0, - "url": "https://ge.xhamster.com/videos/arschlocher-perversa-1990-xhtRIZQ", - "review": 0, - "should_download": 0, - "title": "Arschlocher - Perversa (1990) | xHamster", - "file_name": "Arschlocher - Perversa (1990) [xhtRIZQ].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a096b607-b352-44d0-afda-26ee0daaf42e.mp4" - }, - { - "id": "a0a6d6f6-c804-4958-b669-664b01cf0edf", - "created_date": "2024-07-25 07:29:44.355542", - "last_modified_date": "2024-07-25 07:29:44.355542", - "version": 0, - "url": "https://ge.xhamster.com/videos/fucking-amongst-friends-5554815", - "review": 0, - "should_download": 0, - "title": "Unter Freunden ficken | xHamster", - "file_name": "Unter Freunden ficken [5554815].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a0a6d6f6-c804-4958-b669-664b01cf0edf.mp4" - }, - { - "id": "a16fca93-afc3-4995-acd6-1332185b8b3f", - "created_date": "2024-07-25 07:29:44.632714", - "last_modified_date": "2024-07-25 07:29:44.632714", - "version": 0, - "url": "https://ge.xhamster.com/videos/bikini-girls-full-movie-xh4LKZb", - "review": 0, - "should_download": 0, - "title": "Bikini-M\u00e4dchen (kompletter Film) | xHamster", - "file_name": "Bikini-M\u00e4dchen (kompletter Film) [xh4LKZb].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a16fca93-afc3-4995-acd6-1332185b8b3f.mp4" - }, - { - "id": "a1758e04-d5b8-49c6-b2da-f4a6aa99c9dd", - "created_date": "2024-07-25 07:29:46.645132", - "last_modified_date": "2024-07-25 07:29:46.645132", - "version": 0, - "url": "https://ge.xhamster.com/videos/i-suck-my-lovers-dick-and-my-husband-licks-my-pussy-xhBXiRj", - "review": 0, - "should_download": 0, - "title": "Ich lutsche den Schwanz meines Liebhabers und mein Ehemann leckt meine Muschi. | xHamster", - "file_name": "Ich lutsche den Schwanz meines Liebhabers und mein Ehemann leckt meine Muschi. [xhBXiRj].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a1758e04-d5b8-49c6-b2da-f4a6aa99c9dd.mp4" - }, - { - "id": "a1d442bb-249d-4ec1-ac8a-10ffc34e1260", - "created_date": "2024-07-25 07:29:46.594634", - "last_modified_date": "2024-07-25 07:29:46.594634", - "version": 0, - "url": "https://ge.xhamster.com/videos/swimming-pool-sex-party-7-4842754", - "review": 0, - "should_download": 0, - "title": "Schwimmbad-Sex-Party 7! | xHamster", - "file_name": "Schwimmbad-Sex-Party 7! [4842754].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a1d442bb-249d-4ec1-ac8a-10ffc34e1260.mp4" - }, - { - "id": "a1f2bbc0-63ea-41bc-9c2f-bf411d0712b5", - "created_date": "2024-07-25 07:29:44.702879", - "last_modified_date": "2024-07-25 07:29:44.702879", - "version": 0, - "url": "https://ge.xhamster.com/videos/teen-orgy-1981-12884726", - "review": 0, - "should_download": 0, - "title": "Teenie-Orgie (1981) | xHamster", - "file_name": "Teenie-Orgie (1981) [12884726].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a1f2bbc0-63ea-41bc-9c2f-bf411d0712b5.mp4" - }, - { - "id": "a2503eff-7568-4e81-988a-fdfbbc5194c9", - "created_date": "2024-07-25 07:29:45.222202", - "last_modified_date": "2024-07-25 07:29:45.222202", - "version": 0, - "url": "https://ge.xhamster.com/videos/vacation-party-15005887", - "review": 0, - "should_download": 0, - "title": "Urlaubsparty. | xHamster", - "file_name": "Urlaubsparty. [15005887].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a2503eff-7568-4e81-988a-fdfbbc5194c9.mp4" - }, - { - "id": "a26c3219-735f-44fb-b8cb-cdeaa6057684", - "created_date": "2024-07-25 07:29:45.636506", - "last_modified_date": "2024-07-25 07:29:45.636506", - "version": 0, - "url": "https://ge.xhamster.com/videos/are-you-sperm-im-sorry-i-accidentally-xhEVovf", - "review": 0, - "should_download": 0, - "title": "Bist du sperma !? Es tut mir leid, dass ich versehentlich ... | xHamster", - "file_name": "Bist du sperma !\uff1f Es tut mir leid, dass ich versehentlich ... [xhEVovf].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a26c3219-735f-44fb-b8cb-cdeaa6057684.mp4" - }, - { - "id": "a271298c-015e-45a7-96b3-f42132cd3d6a", - "created_date": "2024-07-25 07:29:48.114805", - "last_modified_date": "2024-07-25 07:29:48.114805", - "version": 0, - "url": "https://ge.xhamster.com/videos/er-schrieb-suche-milf-urlaubsbegleitung-fuer-sex-jeden-tag-hat-er-mich-mehrmals-gebumst-xh1Lzmk", - "review": 0, - "should_download": 0, - "title": "Er Schrieb Suche MILF Urlaubsbegleitung Fuer Sex Jeden Tag Hat Er Mich Mehrmals Gebumst | xHamster", - "file_name": "Er schrieb Suche MILF Urlaubsbegleitung fuer Sex Jeden Tag hat er mich mehrmals gebumst [xh1Lzmk].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a271298c-015e-45a7-96b3-f42132cd3d6a.mp4" - }, - { - "id": "a2deae4a-ffe4-421a-adec-09b9b9a88c23", - "created_date": "2024-07-25 07:29:44.323597", - "last_modified_date": "2024-07-25 07:29:44.323597", - "version": 0, - "url": "https://ge.xhamster.com/videos/a-very-shy-milf-has-to-swallow-her-bosss-cum-to-get-hired-for-in-a-job-interview-xhcgbvO", - "review": 0, - "should_download": 0, - "title": "Eine sehr sch\u00fcchterne milf muss das sperma ihres chefs schlucken, um in einem vorstellungsgespr\u00e4ch eingestellt zu werden | xHamster", - "file_name": "Eine sehr sch\u00fcchterne milf muss das sperma ihres chefs schlucken, um in einem vorstellungsgespr\u00e4ch eingestellt zu werden [xhcgbvO].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/a2deae4a-ffe4-421a-adec-09b9b9a88c23.mp4" - }, - { - "id": "a3109427-1055-46b8-81d8-4b08cfb182b2", - "created_date": "2024-07-25 07:29:45.807677", - "last_modified_date": "2024-07-25 07:29:45.807677", - "version": 0, - "url": "https://ge.xhamster.com/videos/ali-rae-naked-pool-party-13682407", - "review": 0, - "should_download": 0, - "title": "Ali Rae nackte Pool-Party | xHamster", - "file_name": "Ali Rae nackte Pool-Party [13682407].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a3109427-1055-46b8-81d8-4b08cfb182b2.mp4" - }, - { - "id": "a3519c37-131b-44f7-8965-f87d37bf0ef4", - "created_date": "2024-07-25 07:29:47.498756", - "last_modified_date": "2024-07-25 07:29:47.498756", - "version": 0, - "url": "https://ge.xhamster.com/videos/all-in-xhE6huv", - "review": 0, - "should_download": 0, - "title": "Alles in !!! | xHamster", - "file_name": "Alles in !!! [xhE6huv].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a3519c37-131b-44f7-8965-f87d37bf0ef4.mp4" - }, - { - "id": "a37d6807-6ccc-436d-946f-4e42fe17a6be", - "created_date": "2024-11-10 16:53:33.495066", - "last_modified_date": "2024-11-10 16:53:33.495066", - "version": 0, - "url": "https://ge.xhamster.com/videos/flying-skirts-1986-france-us-dub-full-movie-dvd-rip-xhfKMkX", - "review": 0, - "should_download": 0, - "title": "Flying Rocks (1986, Frankreich, US-Synchron, kompletter Film, DVD-Rip) | xHamster", - "file_name": "Flying Rocks (1986, Frankreich, US-Synchron, kompletter Film, DVD-Rip) [xhfKMkX].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/a37d6807-6ccc-436d-946f-4e42fe17a6be.mp4" - }, - { - "id": "a3d7b30f-dea9-408f-aa23-f5fdcaf30b66", - "created_date": "2024-07-25 07:29:44.812168", - "last_modified_date": "2024-07-25 07:29:44.812168", - "version": 0, - "url": "https://ge.xhamster.com/videos/strip-fuck-it-with-malloy-chuck-candle-simon-and-addie-1828233", - "review": 0, - "should_download": 0, - "title": "Strip-Fick mit Malloy, Chuck, Candle, Simon und Addie ( | xHamster", - "file_name": "Strip-Fick mit Malloy, Chuck, Candle, Simon und Addie ( [1828233].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a3d7b30f-dea9-408f-aa23-f5fdcaf30b66.mp4" - }, - { - "id": "a3e24028-d4e7-4eda-8cbd-80443ec8eb63", - "created_date": "2025-01-16 19:59:44.564134", - "last_modified_date": "2025-01-16 19:59:44.564141", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-fun-11182011", - "review": 0, - "should_download": 0, - "title": "Familienspa\u00df | xHamster", - "file_name": "Familienspa\u00df [11182011].mp4", - "path": null, - "cloud_link": "/data/media/a3e24028-d4e7-4eda-8cbd-80443ec8eb63.mp4" - }, - { - "id": "a40aa8f5-c9c6-4565-9bc0-f1784305e806", - "created_date": "2024-07-25 07:29:44.850120", - "last_modified_date": "2024-07-25 07:29:44.850120", - "version": 0, - "url": "https://ge.xhamster.com/videos/madchen-von-nebenan-1-xh4FGUaG", - "review": 0, - "should_download": 0, - "title": "Madchen Von Nebenan 1, Free In German Porn e0 | xHamster", - "file_name": "Madchen von nebenan 1 [xh4FGUaG].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/a40aa8f5-c9c6-4565-9bc0-f1784305e806.mp4" - }, - { - "id": "a434d04d-c03c-43dc-8666-c1561f212c41", - "created_date": "2024-07-25 07:29:47.521419", - "last_modified_date": "2024-07-25 07:29:47.521419", - "version": 0, - "url": "https://ge.xhamster.com/videos/sisswap-naughty-best-friends-both-had-lusty-feeling-for-each-other-s-stepbros-swapped-to-fuck-them-xhFBLWG", - "review": 0, - "should_download": 0, - "title": "Sisswap ist eine freche beste freunde, beide hatten lustvolles gef\u00fchl f\u00fcr die stiefbruer des anderen, getauscht, um sie zu ficken | xHamster", - "file_name": "Sisswap ist eine freche beste freunde, beide hatten lustvolles gef\u00fchl f\u00fcr die stiefbruer des anderen, getauscht, um sie zu ficken [xhFBLWG].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a434d04d-c03c-43dc-8666-c1561f212c41.mp4" - }, - { - "id": "a46edafe-c36c-4f9f-ad03-ef907867ed08", - "created_date": "2024-07-25 07:29:45.333273", - "last_modified_date": "2024-07-25 07:29:45.333273", - "version": 0, - "url": "https://ge.xhamster.com/videos/nasse-junge-schnecken-1992-full-movie-xhOtQro", - "review": 0, - "should_download": 0, - "title": "Nasse Junge Schnecken 1992 Full Movie, Porn eb | xHamster", - "file_name": "Nasse Junge Schnecken 1992 Full Movie [xhOtQro].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/a46edafe-c36c-4f9f-ad03-ef907867ed08.mp4" - }, - { - "id": "a499bc36-019f-4f40-969e-975629ab678b", - "created_date": "2024-07-25 07:29:47.937782", - "last_modified_date": "2024-07-25 07:29:47.937782", - "version": 0, - "url": "https://ge.xhamster.com/videos/big-boobed-stepsister-sneaks-in-your-room-xhFgp7k", - "review": 0, - "should_download": 0, - "title": "Stiefschwester mit gro\u00dfen M\u00f6psen schleicht sich in dein Zimmer | xHamster", - "file_name": "Stiefschwester mit gro\u00dfen M\u00f6psen schleicht sich in dein Zimmer [xhFgp7k].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a499bc36-019f-4f40-969e-975629ab678b.mp4" - }, - { - "id": "a4a3af84-7661-4bb4-b790-bd1759ad9749", - "created_date": "2025-01-16 19:59:49.625422", - "last_modified_date": "2025-01-16 19:59:49.625428", - "version": 0, - "url": "https://ge.xhamster.com/videos/forbidden-desires-xhcmdLt", - "review": 0, - "should_download": 0, - "title": "Verbotene W\u00fcnsche | xHamster", - "file_name": "Verbotene W\u00fcnsche [xhcmdLt].mp4", - "path": null, - "cloud_link": "/data/media/a4a3af84-7661-4bb4-b790-bd1759ad9749.mp4" - }, - { - "id": "a4ac8fdb-9757-4e37-af58-8b2a2175f32c", - "created_date": "2024-07-25 07:29:46.729526", - "last_modified_date": "2024-07-25 07:29:46.729526", - "version": 0, - "url": "https://ge.xhamster.com/videos/watching-porn-with-my-hot-stepmom-ended-up-fucking-her-xhqxPt7", - "review": 0, - "should_download": 0, - "title": "Porno gucken mit meiner hei\u00dfen stiefmutter - ich habe sie am ende gefickt | xHamster", - "file_name": "Porno gucken mit meiner hei\u00dfen stiefmutter - ich habe sie am ende gefickt [xhqxPt7].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a4ac8fdb-9757-4e37-af58-8b2a2175f32c.mp4" - }, - { - "id": "a5ac88d8-b211-49cf-a056-24aa49a07aae", - "created_date": "2024-07-25 07:29:46.328206", - "last_modified_date": "2024-07-25 07:29:46.328206", - "version": 0, - "url": "https://ge.xhamster.com/videos/strict-stepdad-destroyed-his-stepdaughters-madi-collins-butt-and-pussy-in-their-rough-threesome-with-maid-crystal-rush-xhhTOEb", - "review": 0, - "should_download": 0, - "title": "Strenge stiefvater zerst\u00f6rte madi Collins hintern und muschi seiner stieftochter in ihrem groben dreier mit zimmerm\u00e4dchen Crystal Rush. | xHamster", - "file_name": "Strenge stiefvater zerst\u00f6rte madi Collins hintern und muschi seiner stieftochter in ihrem groben dreier mit zimmerm\u00e4dchen Crystal Rush. [xhhTOEb].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a5ac88d8-b211-49cf-a056-24aa49a07aae.mp4" - }, - { - "id": "a5b1907a-3bda-47f1-b096-bb3f81b1b8ef", - "created_date": "2024-07-25 07:29:44.768803", - "last_modified_date": "2024-07-25 07:29:44.768803", - "version": 0, - "url": "https://ge.xhamster.com/videos/sex-addicted-secretary-masturbates-a-lot-while-at-work-xh9IYht", - "review": 0, - "should_download": 0, - "title": "Sexs\u00fcchtige Sekret\u00e4rin masturbiert viel w\u00e4hrend der Arbeit | xHamster", - "file_name": "Sexs\u00fcchtige Sekret\u00e4rin masturbiert viel w\u00e4hrend der Arbeit [xh9IYht].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a5b1907a-3bda-47f1-b096-bb3f81b1b8ef.mp4" - }, - { - "id": "a5d9e6bc-0f0e-421e-aceb-f231ae0ff123", - "created_date": "2024-07-25 07:29:44.570789", - "last_modified_date": "2024-07-25 07:29:44.570789", - "version": 0, - "url": "https://ge.xhamster.com/videos/hot-student-gets-fucked-by-busty-hentai-teacher-xhO6Zdc", - "review": 0, - "should_download": 0, - "title": "Hei\u00dfe Studentin wird von vollbusiger Hentai-Lehrerin gefickt | xHamster", - "file_name": "Hei\u00dfe Studentin wird von vollbusiger Hentai-Lehrerin gefickt [xhO6Zdc].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a5d9e6bc-0f0e-421e-aceb-f231ae0ff123.mp4" - }, - { - "id": "a6064845-5409-4865-9bbe-21e65c662e9c", - "created_date": "2024-11-10 16:53:33.489417", - "last_modified_date": "2024-11-10 16:53:33.489417", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-diary-1999-xhtTgTN", - "review": 0, - "should_download": 0, - "title": "Das Tagebuch (1999) | xHamster", - "file_name": "Das Tagebuch (1999) [xhtTgTN].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/a6064845-5409-4865-9bbe-21e65c662e9c.mp4" - }, - { - "id": "a6290082-53b4-4489-9d23-8a3704f5ac82", - "created_date": "2024-07-25 07:29:46.922354", - "last_modified_date": "2024-07-25 07:29:46.922354", - "version": 0, - "url": "https://ge.xhamster.com/videos/i-confuse-my-horny-stepsister-with-my-girlfriend-and-i-end-up-fucking-her-hard-until-i-cum-in-her-xhkhlpc", - "review": 0, - "should_download": 0, - "title": "Ich verwechsle meine geile Stiefschwester mit meiner Freundin und ich ficke sie am Ende hart, bis ich in sie komme | xHamster", - "file_name": "Ich verwechsle meine geile Stiefschwester mit meiner Freundin und ich ficke sie am Ende hart, bis ich in sie komme [xhkhlpc].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a6290082-53b4-4489-9d23-8a3704f5ac82.mp4" - }, - { - "id": "a66f1640-fc35-4163-a2b7-aa9004d0bdd5", - "created_date": "2024-07-25 07:29:45.982409", - "last_modified_date": "2024-07-25 07:29:45.982409", - "version": 0, - "url": "https://ge.xhamster.com/videos/mom-takes-charge-s8-e8-xhIwZux", - "review": 0, - "should_download": 0, - "title": "Mutter \u00fcbernimmt die f\u00fchrung - s8: e8 | xHamster", - "file_name": "Mutter \u00fcbernimmt die f\u00fchrung - s8\uff1a e8 [xhIwZux].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a66f1640-fc35-4163-a2b7-aa9004d0bdd5.mp4" - }, - { - "id": "a67e2cad-d534-400f-848c-0da300f973b5", - "created_date": "2024-07-25 07:29:46.188633", - "last_modified_date": "2024-07-25 07:29:46.188633", - "version": 0, - "url": "https://ge.xhamster.com/videos/lily-larimar-tells-stepbro-your-going-to-have-to-take-all-of-your-clothes-off-s14-e1-xhtoeBA", - "review": 0, - "should_download": 0, - "title": "Lily Larimar sagt zu Stiefbruder: \"Du musst alle deine Kleider ausziehen m\u00fcssen\" - s14: e1 | xHamster", - "file_name": "Lily Larimar sagt zu Stiefbruder\uff1a \uff02Du musst alle deine Kleider ausziehen m\u00fcssen\uff02 - s14\uff1a e1 [xhtoeBA].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a67e2cad-d534-400f-848c-0da300f973b5.mp4" - }, - { - "id": "a6982216-26b0-4d5e-9ce4-0a073fd3c776", - "created_date": "2024-07-25 07:29:44.555554", - "last_modified_date": "2024-07-25 07:29:44.555554", - "version": 0, - "url": "https://ge.xhamster.com/videos/summertime-blossom-part-1-a-new-blake-aften-opal-hazel-moore-blake-blossom-ginger-grey-bffs-xhqFPrM", - "review": 0, - "should_download": 0, - "title": "Summertime blossom teil 1: a new blake - oft opal, hazel moore, blake blossom, ginger grey - BFFS | xHamster", - "file_name": "Summertime blossom teil 1\uff1a a new blake - oft opal, hazel moore, blake blossom, ginger grey - BFFS [xhqFPrM].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a6982216-26b0-4d5e-9ce4-0a073fd3c776.mp4" - }, - { - "id": "a6e22a1b-5b5e-42ef-b495-e599654cdaf6", - "created_date": "2024-07-25 07:29:47.008739", - "last_modified_date": "2025-01-03 11:56:34.026000", - "version": 1, - "url": "https://ge.xhamster.com/videos/summer-of-72-higher-quality-14494064", - "review": 0, - "should_download": 0, - "title": "Sommer 1972 (h\u00f6here Qualit\u00e4t) | xHamster", - "file_name": "Sommer 1972 (h\u00f6here Qualit\u00e4t) [14494064].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a6e22a1b-5b5e-42ef-b495-e599654cdaf6.mp4" - }, - { - "id": "a7130fe6-2a75-4847-9195-bd49c55c6fdb", - "created_date": "2024-07-25 07:29:46.821279", - "last_modified_date": "2024-07-25 07:29:46.821279", - "version": 0, - "url": "https://ge.xhamster.com/videos/fuck-my-wife-with-me-2-5527462", - "review": 0, - "should_download": 0, - "title": "Fick meine Frau mit mir # 2 | xHamster", - "file_name": "Fick meine Frau mit mir # 2 [5527462].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a7130fe6-2a75-4847-9195-bd49c55c6fdb.mp4" - }, - { - "id": "a78ec6c1-99c3-43dd-a2f5-92b60a7a26d7", - "created_date": "2024-07-25 07:29:45.393655", - "last_modified_date": "2024-07-25 07:29:45.393655", - "version": 0, - "url": "https://ge.xhamster.com/videos/college-teens-fuck-in-dorm-group-with-coeds-7373208", - "review": 0, - "should_download": 0, - "title": "College-Teenager ficken in Wohnheim-Gruppe mit Studentinnen | xHamster", - "file_name": "College-Teenager ficken in Wohnheim-Gruppe mit Studentinnen [7373208].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a78ec6c1-99c3-43dd-a2f5-92b60a7a26d7.mp4" - }, - { - "id": "a7eefa49-4e9b-47d7-a696-46cf80e2df25", - "created_date": "2024-07-25 07:29:44.420704", - "last_modified_date": "2024-07-25 07:29:44.420704", - "version": 0, - "url": "https://ge.xhamster.com/videos/sexboat-1980-xhTuL9v", - "review": 0, - "should_download": 0, - "title": "Sexboat (1980) | xHamster", - "file_name": "Sexboat (1980) [xhTuL9v].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a7eefa49-4e9b-47d7-a696-46cf80e2df25.mp4" - }, - { - "id": "a80f1c63-ff2c-482b-876f-df4f2fbfd9d5", - "created_date": "2024-07-25 07:29:47.243258", - "last_modified_date": "2024-07-25 07:29:47.243258", - "version": 0, - "url": "https://ge.xhamster.com/videos/devilsfilm-rich-neighbors-swap-wives-during-football-game-xhFqS2u", - "review": 0, - "should_download": 0, - "title": "In Devilsfilm tauschen reiche Nachbarn beim Fu\u00dfballspiel Ehefrauen aus | xHamster", - "file_name": "In Devilsfilm tauschen reiche Nachbarn beim Fu\u00dfballspiel Ehefrauen aus [xhFqS2u].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a80f1c63-ff2c-482b-876f-df4f2fbfd9d5.mp4" - }, - { - "id": "a86c6c7b-5633-49cf-a68e-867b268f5dce", - "created_date": "2024-07-25 07:29:46.614645", - "last_modified_date": "2024-07-25 07:29:46.614645", - "version": 0, - "url": "https://ge.xhamster.com/videos/you-can-join-me-if-you-want-to-kasey-miller-12975348", - "review": 0, - "should_download": 0, - "title": "Sie k\u00f6nnen sich mir anschlie\u00dfen, wenn Sie wollen - Kasey Miller | xHamster", - "file_name": "Sie k\u00f6nnen sich mir anschlie\u00dfen, wenn Sie wollen - Kasey Miller [12975348].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a86c6c7b-5633-49cf-a68e-867b268f5dce.mp4" - }, - { - "id": "a879ec91-4545-4135-90d4-5f47f2f812e6", - "created_date": "2024-11-10 16:53:33.495875", - "last_modified_date": "2024-11-10 16:53:33.495875", - "version": 0, - "url": "https://ge.xhamster.com/videos/hallo-taxi-full-movie-xhJL30l", - "review": 0, - "should_download": 0, - "title": "Hallo Taxi! (kompletter Film) | xHamster", - "file_name": "Hallo Taxi! (kompletter Film) [xhJL30l].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/a879ec91-4545-4135-90d4-5f47f2f812e6.mp4" - }, - { - "id": "a8df179e-fa46-493c-a6f8-1fba6b3a901f", - "created_date": "2024-09-24 08:11:39.008727", - "last_modified_date": "2024-10-21 16:31:13.307000", - "version": 1, - "url": "https://ge.xhamster.com/videos/die-pension-mit-der-geilen-milf-5718112", - "review": 0, - "should_download": 0, - "title": "Die Pension Mit Der Geilen MILF, Free HD Porn 62 | xHamster", - "file_name": "Die Pension mit der geilen Milf [5718112].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/a8df179e-fa46-493c-a6f8-1fba6b3a901f.mp4" - }, - { - "id": "a8e87e11-e015-404e-a8e6-397645ada52c", - "created_date": "2024-07-25 07:29:46.535764", - "last_modified_date": "2024-07-25 07:29:46.535764", - "version": 0, - "url": "https://ge.xhamster.com/videos/mom-says-april-fools-your-dick-was-inside-me-xh1HNRC", - "review": 0, - "should_download": 0, - "title": "Mutter sagt, \"Aprilscherz? Dein Schwanz war in mir!\" | xHamster", - "file_name": "Mutter sagt, "Aprilscherz\uff1f Dein Schwanz war in mir!" [xh1HNRC].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a8e87e11-e015-404e-a8e6-397645ada52c.mp4" - }, - { - "id": "a8ee4910-d674-4147-a63d-4f82db7e8994", - "created_date": "2024-07-25 07:29:44.255302", - "last_modified_date": "2024-07-25 07:29:44.255302", - "version": 0, - "url": "https://ge.xhamster.com/videos/dirty-teens-start-the-summer-right-with-some-sex-party-6424570", - "review": 0, - "should_download": 0, - "title": "Schmutzige Teenager beginnen den Sommer direkt mit einer Sexparty | xHamster", - "file_name": "Schmutzige Teenager beginnen den Sommer direkt mit einer Sexparty [6424570].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a8ee4910-d674-4147-a63d-4f82db7e8994.mp4" - }, - { - "id": "a8f0a31e-a958-4d6b-9496-fdc6cf919a86", - "created_date": "2024-07-25 07:29:44.949902", - "last_modified_date": "2024-07-25 07:29:44.949902", - "version": 0, - "url": "https://ge.xhamster.com/videos/horny-slut-seduces-her-roommate-and-makes-him-cum-xhByTjb", - "review": 0, - "should_download": 0, - "title": "Notgeile Schlampe verf\u00fchrt ihren Mitbewohner und bring ihn zum Abspritzen | xHamster", - "file_name": "Notgeile Schlampe verf\u00fchrt ihren Mitbewohner und bring ihn zum Abspritzen [xhByTjb].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/a8f0a31e-a958-4d6b-9496-fdc6cf919a86.mp4" - }, - { - "id": "a8f5e91f-0266-46eb-a627-4383fdb96b28", - "created_date": "2024-07-25 07:29:45.215101", - "last_modified_date": "2024-07-25 07:29:45.215101", - "version": 0, - "url": "https://ge.xhamster.com/videos/a-work-accident-become-a-scene-sex-at-the-office-quick-fuck-boss-and-secretary-he-cum-twice-in-her-pussy-xhUnLbU", - "review": 0, - "should_download": 0, - "title": "Ein Arbeitsunfall wird zur Sexszene im B\u00fcro. Schneller Fick zwischen Chef und Sekret\u00e4rin, er spritzt ihr zweimal in die Muschi | xHamster", - "file_name": "Ein Arbeitsunfall wird zur Sexszene im B\u00fcro. Schneller Fick zwischen Chef und Sekret\u00e4rin, er spritzt ihr zweimal in die Muschi [xhUnLbU].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a8f5e91f-0266-46eb-a627-4383fdb96b28.mp4" - }, - { - "id": "a98f6751-63b4-4b21-855a-01d798b1d905", - "created_date": "2024-07-25 07:29:48.033800", - "last_modified_date": "2024-07-25 07:29:48.033800", - "version": 0, - "url": "https://ge.xhamster.com/videos/meine-suendhafte-tante-xh7YP9d", - "review": 0, - "should_download": 0, - "title": "Meine Suendhafte Tante, Free Porn Video 0b | xHamster", - "file_name": "Meine Suendhafte Tante [xh7YP9d].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a98f6751-63b4-4b21-855a-01d798b1d905.mp4" - }, - { - "id": "a9b70978-8ea1-41a0-8c97-5c203df401a8", - "created_date": "2024-07-25 07:29:45.629393", - "last_modified_date": "2024-07-25 07:29:45.629393", - "version": 0, - "url": "https://ge.xhamster.com/videos/geiler-vierer-im-zug-1377950", - "review": 0, - "should_download": 0, - "title": "Geiler Vierer Im Zug: Free Amateur Porn Video cb | xHamster", - "file_name": "Geiler Vierer im Zug [1377950].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a9b70978-8ea1-41a0-8c97-5c203df401a8.mp4" - }, - { - "id": "a9d69e97-8462-46c5-a031-1ff1f5fb9bbc", - "created_date": "2024-07-25 07:29:44.303800", - "last_modified_date": "2024-07-25 07:29:44.303800", - "version": 0, - "url": "https://ge.xhamster.com/videos/dancing-in-the-rain-4963585", - "review": 0, - "should_download": 0, - "title": "Tanzen im Regen | xHamster", - "file_name": "Tanzen im Regen [4963585].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a9d69e97-8462-46c5-a031-1ff1f5fb9bbc.mp4" - }, - { - "id": "a9fc39b4-682f-44a4-b80e-f91732556531", - "created_date": "2024-07-25 07:29:46.937813", - "last_modified_date": "2024-07-25 07:29:46.937813", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsis-misses-the-cock-xhELzCj", - "review": 0, - "should_download": 0, - "title": "Stiefschwester vermisst den Schwanz | xHamster", - "file_name": "Stiefschwester vermisst den Schwanz [xhELzCj].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/a9fc39b4-682f-44a4-b80e-f91732556531.mp4" - }, - { - "id": "aa3c38d6-110f-40bd-b3e1-b13face62221", - "created_date": "2024-07-25 07:29:44.670678", - "last_modified_date": "2024-07-25 07:29:44.670678", - "version": 0, - "url": "https://ge.xhamster.com/videos/german-teen-picked-up-at-beach-for-threesome-ffm-xhloGpt", - "review": 0, - "should_download": 0, - "title": "Deutsche teen anal abgeschleppt am strand zum Dreier ffm | xHamster", - "file_name": "Deutsche teen anal abgeschleppt am strand zum Dreier ffm [xhloGpt].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/aa3c38d6-110f-40bd-b3e1-b13face62221.mp4" - }, - { - "id": "aa8e2700-521f-4b6e-9beb-c5619c7bdacd", - "created_date": "2024-07-25 07:29:46.336250", - "last_modified_date": "2024-07-25 07:29:46.336250", - "version": 0, - "url": "https://ge.xhamster.com/videos/bride4k-wrong-but-kinda-right-xh3FIwK", - "review": 0, - "should_download": 0, - "title": "Bride4k. Falsch, aber irgendwie richtig | xHamster", - "file_name": "Bride4k. Falsch, aber irgendwie richtig [xh3FIwK].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/aa8e2700-521f-4b6e-9beb-c5619c7bdacd.mp4" - }, - { - "id": "aaa02634-512d-4e27-9309-d182ab3045a7", - "created_date": "2024-07-25 07:29:45.934040", - "last_modified_date": "2024-07-25 07:29:45.934040", - "version": 0, - "url": "https://ge.xhamster.com/videos/hot-foursome-with-double-penetration-12476216", - "review": 0, - "should_download": 0, - "title": "Hei\u00dfer Vierer mit Doppelpenetration | xHamster", - "file_name": "Hei\u00dfer Vierer mit Doppelpenetration [12476216].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/aaa02634-512d-4e27-9309-d182ab3045a7.mp4" - }, - { - "id": "aaf0d8de-72f6-4e1f-8c06-404140f8630b", - "created_date": "2024-07-25 07:29:46.042118", - "last_modified_date": "2024-07-25 07:29:46.042118", - "version": 0, - "url": "https://ge.xhamster.com/videos/erotic-dinner-party-turns-into-orgy-upscaled-to-4k-xhVAZGT", - "review": 0, - "should_download": 0, - "title": "Erotische Dinnerpartys werden zu Orgien, auf 4k hochskaliert | xHamster", - "file_name": "Erotische Dinnerpartys werden zu Orgien, auf 4k hochskaliert [xhVAZGT].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/aaf0d8de-72f6-4e1f-8c06-404140f8630b.mp4" - }, - { - "id": "ab1c8d88-1724-4444-b4a0-19c4ca0f3ed3", - "created_date": "2024-08-28 23:21:54.354190", - "last_modified_date": "2024-08-28 23:21:54.354190", - "version": 0, - "url": "https://ge.xhamster.com/videos/sex-party-after-dinner-2559646", - "review": 0, - "should_download": 0, - "title": "Sexparty nach dem Abendessen | xHamster", - "file_name": "Sexparty nach dem Abendessen [2559646].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/ab1c8d88-1724-4444-b4a0-19c4ca0f3ed3.mp4" - }, - { - "id": "abab052a-8b10-4cfc-a922-a8333e0bb364", - "created_date": "2024-12-29 23:53:27.929108", - "last_modified_date": "2024-12-29 23:53:27.929108", - "version": 0, - "url": "https://ge.xhamster.com/videos/daddy-xh3DupT", - "review": 0, - "should_download": 0, - "title": "Papi | xHamster", - "file_name": "Papi [xh3DupT].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/abab052a-8b10-4cfc-a922-a8333e0bb364.mp4" - }, - { - "id": "abd02714-d938-43bb-8039-dace47e6249e", - "created_date": "2024-07-25 07:29:47.461434", - "last_modified_date": "2024-07-25 07:29:47.461434", - "version": 0, - "url": "https://ge.xhamster.desi/videos/religious-stepmom-kit-mercer-invites-stepdaughter-allie-nicole-for-a-midnight-obedience-lesson-xhDS8Iy", - "review": 0, - "should_download": 0, - "title": "Religi\u00f6se Stiefmutter Mercer l\u00e4dt Stieftochter Allie Nicole zu einer Mitternachtsgehorsam-Lektion ein | xHamster", - "file_name": "Religi\u00f6se Stiefmutter Mercer l\u00e4dt Stieftochter Allie Nicole zu einer Mitternachtsgehorsam-Lektion ein [xhDS8Iy].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/abd02714-d938-43bb-8039-dace47e6249e.mp4" - }, - { - "id": "ac203249-b982-4070-955a-39dd33cd01a9", - "created_date": "2024-12-29 23:53:27.927365", - "last_modified_date": "2024-12-29 23:53:27.927365", - "version": 0, - "url": "https://ge.xhamster.com/videos/botr-tonights-the-night-for-a-family-fuck-11677103", - "review": 0, - "should_download": 0, - "title": "Botr, heute Abend ist die Nacht f\u00fcr einen Familienfick! | xHamster", - "file_name": "Botr, heute Abend ist die Nacht f\u00fcr einen Familienfick! [11677103].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/ac203249-b982-4070-955a-39dd33cd01a9.mp4" - }, - { - "id": "ac98985a-5054-4a98-8969-a7f64a1f487b", - "created_date": "2024-10-21 15:08:43.547204", - "last_modified_date": "2024-10-21 16:31:20.049000", - "version": 1, - "url": "https://ge.xhamster.com/videos/two-amazing-looking-chicks-from-germany-pleasing-some-hard-cocks-at-the-bar-xh5cnma", - "review": 0, - "should_download": 0, - "title": "Zwei erstaunlich aussehende K\u00fcken aus Deutschland erfreuen einige harte Schw\u00e4nze an der Bar | xHamster", - "file_name": "Zwei erstaunlich aussehende K\u00fcken aus Deutschland erfreuen einige harte Schw\u00e4nze an der Bar [xh5cnma].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/ac98985a-5054-4a98-8969-a7f64a1f487b.mp4" - }, - { - "id": "acbf1549-0cfd-4fb5-acb1-032c15b55f12", - "created_date": "2024-07-25 07:29:46.514503", - "last_modified_date": "2024-07-25 07:29:46.514503", - "version": 0, - "url": "https://ge.xhamster.com/videos/das-sex-abitur-versaute-schulmadchen-traume-2-1978-5850792", - "review": 0, - "should_download": 0, - "title": "Das Sex Abitur - Versaute Schulmadchen Traume 2 1978 | xHamster", - "file_name": "Das Sex Abitur - Versaute Schulmadchen Traume 2 (1978) [5850792].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/acbf1549-0cfd-4fb5-acb1-032c15b55f12.mp4" - }, - { - "id": "acf0ecf9-699c-4f97-9d14-c8f66ee1e5ea", - "created_date": "2024-10-21 15:08:43.552109", - "last_modified_date": "2024-10-21 16:31:25.892000", - "version": 1, - "url": "https://ge.xhamster.com/videos/horny-housewife-rammed-by-2-guys-xhiRKFa", - "review": 0, - "should_download": 0, - "title": "Geile Hausfrau von 2 Typen gerammt | xHamster", - "file_name": "Geile Hausfrau von 2 Typen gerammt [xhiRKFa].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/acf0ecf9-699c-4f97-9d14-c8f66ee1e5ea.mp4" - }, - { - "id": "ad061d6e-e11a-47a8-801a-8be95886c030", - "created_date": "2024-07-25 07:29:45.922920", - "last_modified_date": "2024-07-25 07:29:45.922920", - "version": 0, - "url": "https://ge.xhamster.com/videos/horny-stepdaughter-daisy-stone-and-milf-athena-anderson-try-anal-foursome-with-stepbrother-and-stepdad-on-wedding-anniversary-xh4cwD4p", - "review": 0, - "should_download": 0, - "title": "Geile stieftochter Daisy Stone und MILF Athena Anderson versuchen anal-vierer mit stiefbruder und stiefvater zum hochzeitstag | xHamster", - "file_name": "Geile stieftochter Daisy Stone und MILF Athena Anderson versuchen anal-vierer mit stiefbruder und stiefvater zum hochzeitstag [xh4cwD4p].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ad061d6e-e11a-47a8-801a-8be95886c030.mp4" - }, - { - "id": "ad06ee1b-046a-4ad9-9b22-694dfe18ff43", - "created_date": "2024-07-25 07:29:47.707918", - "last_modified_date": "2024-07-25 07:29:47.707918", - "version": 0, - "url": "https://ge.xhamster.com/videos/auf-der-heidi-gibts-koa-sund-teil-1-8550043", - "review": 0, - "should_download": 0, - "title": "Auf Der Heidi Gibts Koa Sund Teil 1, Free Porn 8b | xHamster", - "file_name": "Auf Der Heidi gibts koa Sund teil 1 [8550043].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ad06ee1b-046a-4ad9-9b22-694dfe18ff43.mp4" - }, - { - "id": "ad0e2485-ea00-4586-9228-069a4c0dae6d", - "created_date": "2024-07-25 07:29:47.303442", - "last_modified_date": "2024-07-25 07:29:47.303442", - "version": 0, - "url": "https://ge.xhamster.com/videos/small-village-horny-schools-11170342", - "review": 0, - "should_download": 0, - "title": "Kleines Dorf, geile Schulen | xHamster", - "file_name": "Kleines Dorf, geile Schulen [11170342].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ad0e2485-ea00-4586-9228-069a4c0dae6d.mp4" - }, - { - "id": "ad319532-4da9-4593-88b3-bd381f513474", - "created_date": "2024-08-16 12:24:28.483000", - "last_modified_date": "2024-08-16 12:24:28.483000", - "version": 0, - "url": "https://ge.xhamster.com/videos/i-had-to-have-sex-with-my-husbands-friend-because-of-a-bet-but-i-got-horny-and-ended-up-doing-dp-with-both-of-them-xhneeoz", - "review": 0, - "should_download": 0, - "title": "Ich musste wegen einer wette sex mit dem freund meines mannes haben, aber ich wurde geil und habe am ende mit beiden doppelpenetration gemacht | xHamster", - "file_name": "Ich musste wegen einer wette sex mit dem freund meines mannes haben, aber ich wurde geil und habe am ende mit beiden doppelpenetration gemacht [xhneeoz].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/ad319532-4da9-4593-88b3-bd381f513474.mp4" - }, - { - "id": "ad4f4ec5-7ef9-4894-86c7-dccb682e467d", - "created_date": "2024-07-25 07:29:47.612962", - "last_modified_date": "2024-07-25 07:29:47.612962", - "version": 0, - "url": "https://ge.xhamster.com/videos/you-cant-fuck-the-guests-but-you-can-fuck-me-madi-collins-tempts-stepbro-s9-e1-xh2jqKB", - "review": 0, - "should_download": 0, - "title": "\"Du kannst die G\u00e4ste nicht ficken, aber du KANNST mich ficken\" Madi Collins verf\u00fchrt Stiefbruder - S9:E1 | xHamster", - "file_name": "\uff02Du kannst die G\u00e4ste nicht ficken, aber du KANNST mich ficken\uff02 Madi Collins verf\u00fchrt Stiefbruder - S9\uff1aE1 [xh2jqKB].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ad4f4ec5-7ef9-4894-86c7-dccb682e467d.mp4" - }, - { - "id": "ada32ec7-eba4-4f32-b7d2-5d1e949f0cb7", - "created_date": "2024-07-25 07:29:45.131833", - "last_modified_date": "2024-07-25 07:29:45.131833", - "version": 0, - "url": "https://ge.xhamster.com/videos/hot-retro-outdoor-group-sex-xhiOaBJ", - "review": 0, - "should_download": 0, - "title": "Hei\u00dfer Retro-Gruppensex im Freien | xHamster", - "file_name": "Hei\u00dfer Retro-Gruppensex im Freien [xhiOaBJ].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ada32ec7-eba4-4f32-b7d2-5d1e949f0cb7.mp4" - }, - { - "id": "adb80061-b9f1-4524-afe6-c2ec4472288d", - "created_date": "2024-07-25 07:29:46.889086", - "last_modified_date": "2024-07-25 07:29:46.889086", - "version": 0, - "url": "https://ge.xhamster.com/videos/bruederchen-komm-fick-mit-mir-3501015", - "review": 0, - "should_download": 0, - "title": "Bruederchen Komm Fick Mit Mir, Free Anal Porn 6d | xHamster", - "file_name": "Bruederchen Komm Fick Mit Mir [3501015].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/adb80061-b9f1-4524-afe6-c2ec4472288d.mp4" - }, - { - "id": "addaabe2-fb53-4d34-a2dc-84f07133a40e", - "created_date": "2024-07-25 07:29:45.646929", - "last_modified_date": "2024-07-25 07:29:45.646929", - "version": 0, - "url": "https://ge.xhamster.com/videos/college-teens-pussyfucked-before-cum-in-mouth-5705076", - "review": 0, - "should_download": 0, - "title": "College-Teenager vor dem Abspritzen in den Mund Muschi gefickt | xHamster", - "file_name": "College-Teenager vor dem Abspritzen in den Mund Muschi gefickt [5705076].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/addaabe2-fb53-4d34-a2dc-84f07133a40e.mp4" - }, - { - "id": "adf672b8-d475-4bbb-9808-3766859b35bf", - "created_date": "2024-10-07 20:47:56.420508", - "last_modified_date": "2024-10-21 16:31:30.267000", - "version": 1, - "url": "https://ge.xhamster.com/videos/anal-sex-danish-schoolgirls-4-9723188", - "review": 0, - "should_download": 0, - "title": "Analsex (d\u00e4nische Schulm\u00e4dchen 4) | xHamster", - "file_name": "Analsex (d\u00e4nische Schulm\u00e4dchen 4) [9723188].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/adf672b8-d475-4bbb-9808-3766859b35bf.mp4" - }, - { - "id": "ae05e7ea-ba22-4936-8317-606570b16ccb", - "created_date": "2024-07-25 07:29:44.532887", - "last_modified_date": "2024-07-25 07:29:44.532887", - "version": 0, - "url": "https://ge.xhamster.com/videos/teen-threesome-with-pepper-and-lexi-10085050", - "review": 0, - "should_download": 0, - "title": "Teenie-Dreier mit Pfeffer und Lexi | xHamster", - "file_name": "Teenie-Dreier mit Pfeffer und Lexi [10085050].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ae05e7ea-ba22-4936-8317-606570b16ccb.mp4" - }, - { - "id": "ae2e76ac-39ee-43b0-8321-badb9e4b0afa", - "created_date": "2024-07-25 07:29:47.040228", - "last_modified_date": "2024-07-25 07:29:47.040228", - "version": 0, - "url": "https://ge.xhamster.com/videos/your-stepdad-cant-even-satisfy-me-xhgHXG4", - "review": 0, - "should_download": 0, - "title": "Dein Stiefvater kann mich nicht einmal befriedigen | xHamster", - "file_name": "Dein Stiefvater kann mich nicht einmal befriedigen [xhgHXG4].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ae2e76ac-39ee-43b0-8321-badb9e4b0afa.mp4" - }, - { - "id": "ae732ddc-dd3f-414b-bd6b-f7d81a1d84a9", - "created_date": "2025-01-16 20:00:04.806579", - "last_modified_date": "2025-01-16 20:00:04.806585", - "version": 0, - "url": "https://ge.xhamster.com/videos/believe-it-or-not-real-step-sisters-dolly-leigh-and-izzy-lush-got-dumped-on-the-same-day-teamskeet-xhmuxO4", - "review": 0, - "should_download": 0, - "title": "Ob Sie es glauben oder nicht, echte stiefschwestern dolly leigh und Izzy lush wurden am selben tag entsorgt -teamSkeet | xHamster", - "file_name": "Ob Sie es glauben oder nicht, echte stiefschwestern dolly leigh und Izzy lush wurden am selben tag entsorgt -teamSkeet [xhmuxO4].mp4", - "path": null, - "cloud_link": "/data/media/ae732ddc-dd3f-414b-bd6b-f7d81a1d84a9.mp4" - }, - { - "id": "aebc3169-db9d-4fd7-a274-90333d2d1942", - "created_date": "2024-07-25 07:29:46.386998", - "last_modified_date": "2024-07-25 07:29:46.386998", - "version": 0, - "url": "https://ge.xhamster.com/videos/barely-legal-03-13858051", - "review": 0, - "should_download": 0, - "title": "Kaum legal # 03 | xHamster", - "file_name": "Kaum legal # 03 [13858051].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/aebc3169-db9d-4fd7-a274-90333d2d1942.mp4" - }, - { - "id": "aeddff77-6cf9-40f3-a505-eced98b071a4", - "created_date": "2024-07-25 07:29:44.444217", - "last_modified_date": "2024-07-25 07:29:44.444217", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsister-wants-to-play-truth-or-dare-e4-touch-my-cock-xhlV9iF", - "review": 0, - "should_download": 0, - "title": "Stiefschwester will Wahrheit oder Pflicht spielen e4: Ber\u00fchre meinen Schwanz | xHamster", - "file_name": "Stiefschwester will Wahrheit oder Pflicht spielen e4\uff1a Ber\u00fchre meinen Schwanz [xhlV9iF].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/aeddff77-6cf9-40f3-a505-eced98b071a4.mp4" - }, - { - "id": "aedef29a-2c48-4f51-9d7b-0220a1d50f81", - "created_date": "2024-07-25 07:29:47.085127", - "last_modified_date": "2024-07-25 07:29:47.085127", - "version": 0, - "url": "https://ge.xhamster.com/videos/heisse-schulmadchenluste-1984-with-anne-karna-9219899", - "review": 0, - "should_download": 0, - "title": "Heisse Schulmadchenluste (1984) mit Anne Karna | xHamster", - "file_name": "Heisse Schulmadchenluste (1984) mit Anne Karna [9219899].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/aedef29a-2c48-4f51-9d7b-0220a1d50f81.mp4" - }, - { - "id": "aef15d32-266d-4d8f-bd00-0a3ce84e3878", - "created_date": "2024-07-25 07:29:44.925513", - "last_modified_date": "2024-07-25 07:29:44.925513", - "version": 0, - "url": "https://ge.xhamster.com/videos/threesome-with-the-big-boobs-secretary-cara-st-germain-xhoXRVq", - "review": 0, - "should_download": 0, - "title": "Dreier mit der vollbusigen sekret\u00e4rin cara st germain | xHamster", - "file_name": "Dreier mit der vollbusigen sekret\u00e4rin cara st germain [xhoXRVq].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/aef15d32-266d-4d8f-bd00-0a3ce84e3878.mp4" - }, - { - "id": "af2b5bcc-983b-450b-8a50-98c74593cc94", - "created_date": "2024-07-25 07:29:47.047597", - "last_modified_date": "2024-07-25 07:29:47.047597", - "version": 0, - "url": "https://ge.xhamster.com/videos/outdoor-adventure-sassy-blonde-loves-masturbating-in-the-woods-so-random-strangers-can-catch-her-xhFqxWu", - "review": 0, - "should_download": 0, - "title": "Outdoor-abenteuer - eine freche blondine liebt es, im wald zu masturbieren, damit zuf\u00e4llige Fremde sie fangen k\u00f6nnen | xHamster", - "file_name": "Outdoor-abenteuer - eine freche blondine liebt es, im wald zu masturbieren, damit zuf\u00e4llige Fremde sie fangen k\u00f6nnen [xhFqxWu].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/af2b5bcc-983b-450b-8a50-98c74593cc94.mp4" - }, - { - "id": "af369219-7e10-4427-b030-d36a7f34a393", - "created_date": "2024-07-25 07:29:46.591033", - "last_modified_date": "2024-07-25 07:29:46.591033", - "version": 0, - "url": "https://ge.xhamster.com/videos/step-moms-bang-teens-naughty-needs-threesome-6470534", - "review": 0, - "should_download": 0, - "title": "Stiefmutter knallt Teenager - Frech braucht Dreier | xHamster", - "file_name": "Stiefmutter knallt Teenager - Frech braucht Dreier [6470534].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/af369219-7e10-4427-b030-d36a7f34a393.mp4" - }, - { - "id": "af6e8c6b-1243-4257-9adb-8d07af507bda", - "created_date": "2024-12-29 23:53:27.935353", - "last_modified_date": "2024-12-29 23:53:27.935353", - "version": 0, - "url": "https://ge.xhamster.com/videos/sweet-honey-14698921", - "review": 0, - "should_download": 0, - "title": "S\u00fc\u00dfer Schatz | xHamster", - "file_name": "S\u00fc\u00dfer Schatz [14698921].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/af6e8c6b-1243-4257-9adb-8d07af507bda.mp4" - }, - { - "id": "b0166a4d-0652-41d5-b428-03e0c7e14317", - "created_date": "2024-07-25 07:29:47.604382", - "last_modified_date": "2024-07-25 07:29:47.604382", - "version": 0, - "url": "https://ge.xhamster.com/videos/chrissy-the-campus-slut-1-xhuRxvd", - "review": 0, - "should_download": 0, - "title": "Chrissy, die Campus-Schlampe # 1 | xHamster", - "file_name": "Chrissy, die Campus-Schlampe # 1 [xhuRxvd].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b0166a4d-0652-41d5-b428-03e0c7e14317.mp4" - }, - { - "id": "b0293303-5492-4143-a802-1d6a171fc28b", - "created_date": "2024-07-25 07:29:47.028354", - "last_modified_date": "2025-01-03 11:56:37.648000", - "version": 1, - "url": "https://ge.xhamster.com/videos/my-boyfriends-dad-shared-his-wisdom-and-i-came-s30-e5-xh6jy53", - "review": 0, - "should_download": 0, - "title": "Der vater meines freundes teilte seine Weisheit und ich kam - S30: E5 | xHamster", - "file_name": "Der vater meines freundes teilte seine Weisheit und ich kam - S30\uff1a E5 [xh6jy53].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b0293303-5492-4143-a802-1d6a171fc28b.mp4" - }, - { - "id": "b0791e4b-a4d2-434f-b2a7-e1b25bac1dc8", - "created_date": "2024-10-21 15:08:43.556218", - "last_modified_date": "2024-10-21 16:31:35.231000", - "version": 1, - "url": "https://ge.xhamster.com/videos/fickparty-und-wildes-treiben-xh7cWy4", - "review": 0, - "should_download": 0, - "title": "Fickparty Und Wildes Treiben, Free Tight Pussy HD Porn 59 | xHamster", - "file_name": "Fickparty und wildes treiben [xh7cWy4].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/b0791e4b-a4d2-434f-b2a7-e1b25bac1dc8.mp4" - }, - { - "id": "b082f26b-f7ce-42bb-a15e-e13ea04ff67a", - "created_date": "2024-07-25 07:29:45.800483", - "last_modified_date": "2024-07-25 07:29:45.800483", - "version": 0, - "url": "https://ge.xhamster.com/videos/public-beach-fucking-on-caribbean-beach-blowjob-public-sex-xhXvUGQ", - "review": 0, - "should_download": 0, - "title": "\u00d6ffentlicher Strandfick am Karibikstrand, Blowjob, Sex in der \u00d6ffentlichkeit | xHamster", - "file_name": "\u00d6ffentlicher Strandfick am Karibikstrand, Blowjob, Sex in der \u00d6ffentlichkeit [xhXvUGQ].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b082f26b-f7ce-42bb-a15e-e13ea04ff67a.mp4" - }, - { - "id": "b08e0677-e616-46ed-b603-0210757c8836", - "created_date": "2024-10-07 20:47:56.418609", - "last_modified_date": "2024-10-21 16:31:38.109000", - "version": 1, - "url": "https://ge.xhamster.com/videos/18-videoz-unplanned-threesome-6988354", - "review": 0, - "should_download": 0, - "title": "18 Videoz - ungeplanter Dreier | xHamster", - "file_name": "18 Videoz - ungeplanter Dreier [6988354].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/b08e0677-e616-46ed-b603-0210757c8836.mp4" - }, - { - "id": "b0bfd9c0-c7e5-4689-8192-fae7ce1c8c7c", - "created_date": "2024-08-09 19:03:49.366496", - "last_modified_date": "2024-08-09 19:03:49.366496", - "version": 0, - "url": "https://ge.xhamster.com/videos/fucking-with-friends-xh088HJ", - "review": 0, - "should_download": 0, - "title": "Ficken mit Freunden | xHamster", - "file_name": "Ficken mit Freunden [xh088HJ].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/b0bfd9c0-c7e5-4689-8192-fae7ce1c8c7c.mp4" - }, - { - "id": "b1210e3c-263d-4fd0-a1dd-8b612a4e5cdf", - "created_date": "2024-07-25 07:29:44.370804", - "last_modified_date": "2024-07-25 07:29:44.370804", - "version": 0, - "url": "https://ge.xhamster.com/videos/hes-much-bigger-then-my-husband-xho6pH1", - "review": 0, - "should_download": 0, - "title": "Er ist viel gr\u00f6\u00dfer als mein Ehemann !. | xHamster", - "file_name": "Er ist viel gr\u00f6\u00dfer als mein Ehemann !. [xho6pH1].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b1210e3c-263d-4fd0-a1dd-8b612a4e5cdf.mp4" - }, - { - "id": "b12cbfa0-ac9c-4c95-8e24-3685ab04d0d0", - "created_date": "2024-07-25 07:29:46.933293", - "last_modified_date": "2024-07-25 07:29:46.933293", - "version": 0, - "url": "https://ge.xhamster.com/videos/im-swingerclub-mich-vom-fremden-mann-bumsen-lassen-xhaT7H1", - "review": 0, - "should_download": 0, - "title": "Im Swingerclub Mich Vom Fremden Mann Bumsen Lassen: Porn ad | xHamster", - "file_name": "Im Swingerclub mich vom fremden Mann bumsen lassen [xhaT7H1].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b12cbfa0-ac9c-4c95-8e24-3685ab04d0d0.mp4" - }, - { - "id": "b19fa638-0c01-426e-80f3-23c2cac18b40", - "created_date": "2024-07-25 07:29:47.318396", - "last_modified_date": "2024-07-25 07:29:47.318396", - "version": 0, - "url": "https://ge.xhamster.com/videos/pool-party-2-guys-and-5-czech-girls-teenrs-com-7853858", - "review": 0, - "should_download": 0, - "title": "Poolparty 2 Typen und 5 tschechische M\u00e4dchen teenrs.com | xHamster", - "file_name": "Poolparty 2 Typen und 5 tschechische M\u00e4dchen teenrs.com [7853858].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b19fa638-0c01-426e-80f3-23c2cac18b40.mp4" - }, - { - "id": "b1b26c98-86e0-479e-82d3-a6a310f05732", - "created_date": "2024-07-25 07:29:46.899856", - "last_modified_date": "2024-07-25 07:29:46.899856", - "version": 0, - "url": "https://ge.xhamster.com/videos/teenies-gerade-18-bildhuebsch-und-sehr-versaut-7955728", - "review": 0, - "should_download": 0, - "title": "Teenies Gerade 18 - Bildhuebsch Und Sehr Versaut: Porn ff | xHamster", - "file_name": "Teenies gerade 18 - Bildhuebsch und sehr versaut [7955728].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b1b26c98-86e0-479e-82d3-a6a310f05732.mp4" - }, - { - "id": "b1cbcd8d-a9d7-4c84-8179-89254432a1f6", - "created_date": "2024-07-25 07:29:46.173453", - "last_modified_date": "2024-07-25 07:29:46.173453", - "version": 0, - "url": "https://ge.xhamster.com/videos/orgy-1978-11901295", - "review": 0, - "should_download": 0, - "title": "Orgie (1978) | xHamster", - "file_name": "Orgie (1978) [11901295].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b1cbcd8d-a9d7-4c84-8179-89254432a1f6.mp4" - }, - { - "id": "b1e9463e-9324-4efd-8f16-7700d867994e", - "created_date": "2024-07-25 07:29:44.620810", - "last_modified_date": "2024-07-25 07:29:44.620810", - "version": 0, - "url": "https://ge.xhamster.com/videos/verbotenes-spiel-1979-6317174", - "review": 0, - "should_download": 0, - "title": "Verbotenes Spiel 1979, Free Orgy Porn Video 63 | xHamster", - "file_name": "Verbotenes Spiel (1979) [6317174].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b1e9463e-9324-4efd-8f16-7700d867994e.mp4" - }, - { - "id": "b1eab02f-29f2-40ca-bb07-04f067124813", - "created_date": "2024-07-25 07:29:44.399820", - "last_modified_date": "2024-07-25 07:29:44.399820", - "version": 0, - "url": "https://ge.xhamster.com/videos/wife-with-friends-1643137", - "review": 0, - "should_download": 0, - "title": "Ehefrau mit Freunden | xHamster", - "file_name": "Ehefrau mit Freunden [1643137].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b1eab02f-29f2-40ca-bb07-04f067124813.mp4" - }, - { - "id": "b20d1361-9925-42aa-9d49-4cb5584f493c", - "created_date": "2024-12-29 23:53:27.885234", - "last_modified_date": "2024-12-29 23:53:27.885234", - "version": 0, - "url": "https://ge.xhamster.com/videos/meine-geile-weihnachtsfeier-episode-4-xhEl6M2", - "review": 0, - "should_download": 0, - "title": "Meine Geile Weihnachtsfeier - Episode 4, Porn 98 | xHamster", - "file_name": "Meine geile weihnachtsfeier - Episode 4 [xhEl6M2].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/b20d1361-9925-42aa-9d49-4cb5584f493c.mp4" - }, - { - "id": "b22ae561-61f9-44c4-af1c-62201baf0c7e", - "created_date": "2024-07-25 07:29:46.030151", - "last_modified_date": "2024-07-25 07:29:46.030151", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-first-threesome-s9-e7-xh7AtHf", - "review": 0, - "should_download": 0, - "title": "Mein erster dreier - s9: e7 | xHamster", - "file_name": "Mein erster dreier - s9\uff1a e7 [xh7AtHf].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b22ae561-61f9-44c4-af1c-62201baf0c7e.mp4" - }, - { - "id": "b2755db1-3727-42f7-9552-1352f5786aaf", - "created_date": "2024-07-25 07:29:46.362247", - "last_modified_date": "2024-07-25 07:29:46.362247", - "version": 0, - "url": "https://ge.xhamster.com/videos/wedding-party-297919", - "review": 0, - "should_download": 0, - "title": "Hochzeitsfeier | xHamster", - "file_name": "Hochzeitsfeier [297919].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b2755db1-3727-42f7-9552-1352f5786aaf.mp4" - }, - { - "id": "b2ab263e-a5fd-40f0-aeec-d3ca8f4e3c68", - "created_date": "2024-11-10 16:53:33.487704", - "last_modified_date": "2024-11-10 16:53:33.487704", - "version": 0, - "url": "https://ge.xhamster.com/videos/miriam-teacher-14218585", - "review": 0, - "should_download": 0, - "title": "Miriam - Lehrerin | xHamster", - "file_name": "Miriam - Lehrerin [14218585].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/b2ab263e-a5fd-40f0-aeec-d3ca8f4e3c68.mp4" - }, - { - "id": "b2f300a0-391b-4a59-8d04-23642f67d43a", - "created_date": "2024-07-25 07:29:47.907080", - "last_modified_date": "2024-07-25 07:29:47.907080", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsisters-bff-says-i-dare-you-to-get-brother-to-show-us-his-dick-xhopwLb", - "review": 0, - "should_download": 0, - "title": "Die Freundin der Stiefschwester sagt, ich wage es, dass du Stiefbruder bekommst, der uns seinen Schwanz zeigt | xHamster", - "file_name": "Die Freundin der Stiefschwester sagt, ich wage es, dass du Stiefbruder bekommst, der uns seinen Schwanz zeigt [xhopwLb].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b2f300a0-391b-4a59-8d04-23642f67d43a.mp4" - }, - { - "id": "b31acd3b-535f-413e-88e5-807780c19c1d", - "created_date": "2024-08-09 19:04:01.562661", - "last_modified_date": "2024-08-09 19:04:01.562661", - "version": 0, - "url": "https://ge.xhamster.com/videos/performance-review-girl-gets-wet-when-boss-fucks-client-xhUXamo", - "review": 0, - "should_download": 0, - "title": "Performance-Review - M\u00e4dchen wird nass, wenn der Chef Kunden fickt | xHamster", - "file_name": "Performance-Review - M\u00e4dchen wird nass, wenn der Chef Kunden fickt [xhUXamo].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/b31acd3b-535f-413e-88e5-807780c19c1d.mp4" - }, - { - "id": "b31d0368-7367-4b7d-81d9-5650e6794d2d", - "created_date": "2024-07-25 07:29:47.427028", - "last_modified_date": "2024-07-25 07:29:47.427028", - "version": 0, - "url": "https://ge.xhamster.com/videos/babes-step-mom-lessons-alexa-tomas-and-cindy-loarn-and-g-8409763", - "review": 0, - "should_download": 0, - "title": "Babes - Stiefmutter-Unterricht - Alexa Tomas und Cindy Loarn und g | xHamster", - "file_name": "Babes - Stiefmutter-Unterricht - Alexa Tomas und Cindy Loarn und g [8409763].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b31d0368-7367-4b7d-81d9-5650e6794d2d.mp4" - }, - { - "id": "b331cb7d-764b-4e36-adaf-3d06ae31098a", - "created_date": "2024-07-25 07:29:44.985492", - "last_modified_date": "2024-07-25 07:29:44.985492", - "version": 0, - "url": "https://ge.xhamster.com/videos/your-panties-are-on-the-floor-what-were-you-guys-just-doing-mandy-waters-asks-dani-diaz-s6-e9-xhUNfON", - "review": 0, - "should_download": 0, - "title": "\"Dein h\u00f6schen ist auf dem Boden. Was haben Sie jungs gerade gemacht?\" Mandy Waters fragt Dani Diaz -S6: E9 | xHamster", - "file_name": "\uff02Dein h\u00f6schen ist auf dem Boden. Was haben Sie jungs gerade gemacht\uff1f\uff02 Mandy Waters fragt Dani Diaz -S6\uff1a E9 [xhUNfON].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b331cb7d-764b-4e36-adaf-3d06ae31098a.mp4" - }, - { - "id": "b339110d-bdf8-482d-9239-06ebc3184077", - "created_date": "2024-11-01 21:08:26.932520", - "last_modified_date": "2024-11-01 21:08:26.932520", - "version": 0, - "url": "https://ge.xhamster.com/videos/german-vintage-5088343", - "review": 0, - "should_download": 0, - "title": "Deutscher Retro | xHamster", - "file_name": "Deutscher Retro [5088343].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/b339110d-bdf8-482d-9239-06ebc3184077.mp4" - }, - { - "id": "b33dc0b1-5a82-41c7-ac4e-09085464d6fc", - "created_date": "2024-12-29 23:53:27.928213", - "last_modified_date": "2024-12-29 23:53:27.928213", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-enjoy-evening-at-home-xhTkwhu", - "review": 0, - "should_download": 0, - "title": "Familie genie\u00dft den Abend zu Hause | xHamster", - "file_name": "Familie genie\u00dft den Abend zu Hause [xhTkwhu].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/b33dc0b1-5a82-41c7-ac4e-09085464d6fc.mp4" - }, - { - "id": "b39a46e0-3f13-405a-a9a8-54e8526661df", - "created_date": "2024-07-25 07:29:45.203500", - "last_modified_date": "2024-07-25 07:29:45.203500", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-story-of-how-i-was-caught-in-bed-with-a-sex-machine-xhGmJYn", - "review": 0, - "should_download": 0, - "title": "Die Geschichte davon, wie ich mit einer sexmaschine im bett erwischt wurde. | xHamster", - "file_name": "Die Geschichte davon, wie ich mit einer sexmaschine im bett erwischt wurde. [xhGmJYn].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b39a46e0-3f13-405a-a9a8-54e8526661df.mp4" - }, - { - "id": "b3c29573-d7b6-4c80-a384-b3eea5296001", - "created_date": "2024-07-25 07:29:47.171718", - "last_modified_date": "2024-07-25 07:29:47.171718", - "version": 0, - "url": "https://ge.xhamster.com/videos/hot-teenage-assets-1978-10059593", - "review": 0, - "should_download": 0, - "title": "Hei\u00dfe Teenager-Assets (1978) | xHamster", - "file_name": "Hei\u00dfe Teenager-Assets (1978) [10059593].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b3c29573-d7b6-4c80-a384-b3eea5296001.mp4" - }, - { - "id": "b46b3122-47c7-4f16-97b1-781130e84afe", - "created_date": "2024-07-25 07:29:45.445966", - "last_modified_date": "2024-07-25 07:29:45.445966", - "version": 0, - "url": "https://ge.xhamster.com/videos/babes-step-mom-lessons-leny-ewil-gina-gerson-kathia-no-8563379", - "review": 0, - "should_download": 0, - "title": "Sch\u00e4tzchen - Stiefmutterunterricht - Leny Ewil, Gina Gerson, Kathia No | xHamster", - "file_name": "Sch\u00e4tzchen - Stiefmutterunterricht - Leny Ewil, Gina Gerson, Kathia No [8563379].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b46b3122-47c7-4f16-97b1-781130e84afe.mp4" - }, - { - "id": "b476d3d6-c511-41e9-8ffc-e1da33b66482", - "created_date": "2024-07-25 07:29:44.688498", - "last_modified_date": "2024-07-25 07:29:44.688498", - "version": 0, - "url": "https://ge.xhamster.com/videos/kendra-spade-caught-wetting-herself-by-stepdad-xhMmVh5", - "review": 0, - "should_download": 0, - "title": "Kendra Spade erwischte sich, als sie sich von Stiefvater benetzte | xHamster", - "file_name": "Kendra Spade erwischte sich, als sie sich von Stiefvater benetzte [xhMmVh5].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b476d3d6-c511-41e9-8ffc-e1da33b66482.mp4" - }, - { - "id": "b48d26a8-4ae1-43b8-9d66-001a759ea69b", - "created_date": "2024-08-09 19:05:14.504690", - "last_modified_date": "2024-08-09 19:05:14.504690", - "version": 0, - "url": "https://ge.xhamster.com/videos/topless-chicks-in-a-pool-with-horny-as-fuck-college-frat-guys-tiny-hole-babe-mia-gold-gets-nailed-by-two-dicks-xhlVpld", - "review": 0, - "should_download": 0, - "title": "Topless Chicks in a Pool with Horny as Fuck College Frat Guys Tiny Hole Babe Mia Gold gets Nailed by Two Dicks | xHamster", - "file_name": "topless chicks in a pool with horny as fuck college frat guys Tiny Hole Babe Mia Gold Gets Nailed by Two Dicks [xhlVpld].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/b48d26a8-4ae1-43b8-9d66-001a759ea69b.mp4" - }, - { - "id": "b48f6dbd-714c-4763-baa8-5c215b4a8fc9", - "created_date": "2024-07-25 07:29:47.247437", - "last_modified_date": "2024-07-25 07:29:47.247437", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-inc-1-full-german-movie-xhBoAf5", - "review": 0, - "should_download": 0, - "title": "Familie inkl. 1 - kompletter deutscher Film | xHamster", - "file_name": "Familie inkl. 1 - kompletter deutscher Film [xhBoAf5].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b48f6dbd-714c-4763-baa8-5c215b4a8fc9.mp4" - }, - { - "id": "b5084dde-0cb2-4055-8a26-cb87e4791303", - "created_date": "2024-07-25 07:29:47.877653", - "last_modified_date": "2024-07-25 07:29:47.877653", - "version": 0, - "url": "https://ge.xhamster.com/videos/chateau-duval-full-original-movie-in-hd-xhAnMFs", - "review": 0, - "should_download": 0, - "title": "Chateau Duval - (kompletter originalfilm in HD) | xHamster", - "file_name": "Chateau Duval - (kompletter originalfilm in HD) [xhAnMFs].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b5084dde-0cb2-4055-8a26-cb87e4791303.mp4" - }, - { - "id": "b50a7f17-b2f6-464b-ba0d-e401b0658d58", - "created_date": "2024-07-25 07:29:48.025482", - "last_modified_date": "2024-07-25 07:29:48.025482", - "version": 0, - "url": "https://ge.xhamster.com/videos/brunette-keeps-all-the-three-cocks-engage-throughout-the-foursome-session-5511018", - "review": 0, - "should_download": 0, - "title": "Br\u00fcnette h\u00e4lt alle drei Schw\u00e4nze w\u00e4hrend der Vierer-Session in Eingriff | xHamster", - "file_name": "Br\u00fcnette h\u00e4lt alle drei Schw\u00e4nze w\u00e4hrend der Vierer-Session in Eingriff [5511018].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b50a7f17-b2f6-464b-ba0d-e401b0658d58.mp4" - }, - { - "id": "b518a416-ee6c-46d0-8bc8-710dd2324910", - "created_date": "2024-07-25 07:29:47.544510", - "last_modified_date": "2024-07-25 07:29:47.544510", - "version": 0, - "url": "https://ge.xhamster.com/videos/junger-ist-enger-ganzer-film-xhdNW8X", - "review": 0, - "should_download": 0, - "title": "Junger Ist Enger Ganzer Film, Free Romantic HD Porn 62 | xHamster", - "file_name": "Junger ist enger (GANZER FILM) [xhdNW8X].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b518a416-ee6c-46d0-8bc8-710dd2324910.mp4" - }, - { - "id": "b51a2ded-0da6-4006-a2f4-d34765f40ac8", - "created_date": "2024-11-10 16:53:33.480594", - "last_modified_date": "2024-11-10 16:53:33.480594", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-young-like-it-hot-1983-us-full-movie-bdrip-xhViCnc", - "review": 0, - "should_download": 0, - "title": "Die Jungen m\u00f6gen es hei\u00df (1983, us, kompletter Film, bdrip) | xHamster", - "file_name": "Die Jungen m\u00f6gen es hei\u00df (1983, us, kompletter Film, bdrip) [xhViCnc].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/b51a2ded-0da6-4006-a2f4-d34765f40ac8.mp4" - }, - { - "id": "b569fa40-68cf-411d-bf99-052bb79da277", - "created_date": "2024-07-25 07:29:45.479895", - "last_modified_date": "2024-07-25 07:29:45.479895", - "version": 0, - "url": "https://ge.xhamster.com/videos/massage-therapist-redhead-cheats-xhydjoy", - "review": 0, - "should_download": 0, - "title": "Rothaarige Masseurin betr\u00fcgt | xHamster", - "file_name": "Rothaarige Masseurin betr\u00fcgt [xhydjoy].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b569fa40-68cf-411d-bf99-052bb79da277.mp4" - }, - { - "id": "b56a0048-e384-4601-8a47-0b6037162f01", - "created_date": "2024-07-25 07:29:45.824952", - "last_modified_date": "2024-07-25 07:29:45.824952", - "version": 0, - "url": "https://ge.xhamster.com/videos/classic-1980-verbotene-gelueste-part-2-xhpFde0", - "review": 0, - "should_download": 0, - "title": "Classic 1980 - Verbotene Gelueste Part 2, Porn de | xHamster", - "file_name": "Classic 1980 - Verbotene Gelueste part 2 [xhpFde0].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b56a0048-e384-4601-8a47-0b6037162f01.mp4" - }, - { - "id": "b572b5a2-358c-4337-82af-1a9a6ba28e80", - "created_date": "2024-07-25 07:29:45.067515", - "last_modified_date": "2024-07-25 07:29:45.067515", - "version": 0, - "url": "https://ge.xhamster.com/videos/big-naturals-teen-enjoys-bbc-outdoors-13070001", - "review": 0, - "should_download": 0, - "title": "Gro\u00dfes nat\u00fcrliches Teen genie\u00dft BBC im Freien | xHamster", - "file_name": "Gro\u00dfes nat\u00fcrliches Teen genie\u00dft BBC im Freien [13070001].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b572b5a2-358c-4337-82af-1a9a6ba28e80.mp4" - }, - { - "id": "b577b017-8003-44ce-9c44-ce4fcd1f6ed5", - "created_date": "2024-07-25 07:29:44.798031", - "last_modified_date": "2024-07-25 07:29:44.798031", - "version": 0, - "url": "https://ge.xhamster.com/videos/adult-time-hotwife-casey-calverts-horny-in-laws-airtight-gangbang-bukkake-her-with-husbands-help-xhn4yql", - "review": 0, - "should_download": 0, - "title": "Adult Time - hotwife casey Calverts geile schwiedermutter, luftige gangbang und bukkake sie mit der hilfe des ehemanns | xHamster", - "file_name": "Adult Time - hotwife casey Calverts geile schwiedermutter, luftige gangbang und bukkake sie mit der hilfe des ehemanns [xhn4yql].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b577b017-8003-44ce-9c44-ce4fcd1f6ed5.mp4" - }, - { - "id": "b5907364-873a-4a77-90cf-fd7d483e1c10", - "created_date": "2024-07-25 07:29:46.102422", - "last_modified_date": "2024-07-25 07:29:46.102422", - "version": 0, - "url": "https://ge.xhamster.com/videos/busty-milf-with-natural-tits-fucks-on-the-beach-11296164", - "review": 0, - "should_download": 0, - "title": "Vollbusige MILF mit nat\u00fcrlichen Titten fickt am Strand | xHamster", - "file_name": "Vollbusige MILF mit nat\u00fcrlichen Titten fickt am Strand [11296164].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b5907364-873a-4a77-90cf-fd7d483e1c10.mp4" - }, - { - "id": "b5ac95d3-ece2-4c05-981c-88ae86ec7fba", - "created_date": "2024-07-25 07:29:45.119817", - "last_modified_date": "2024-07-25 07:29:45.119817", - "version": 0, - "url": "https://ge.xhamster.com/videos/orgy-with-lots-of-cumshots-802168", - "review": 0, - "should_download": 0, - "title": "Orgie mit vielen Cumshots | xHamster", - "file_name": "Orgie mit vielen Cumshots [802168].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b5ac95d3-ece2-4c05-981c-88ae86ec7fba.mp4" - }, - { - "id": "b5ceb5af-2318-48e7-9c83-aec81a0ff0e2", - "created_date": "2024-11-10 16:53:33.496795", - "last_modified_date": "2024-11-10 16:53:33.496795", - "version": 0, - "url": "https://ge.xhamster.com/videos/intimacy-vol-2-full-original-movie-xh0omub", - "review": 0, - "should_download": 0, - "title": "Intimacy vol.2 (kompletter Originalfilm) | xHamster", - "file_name": "Intimacy vol.2 (kompletter Originalfilm) [xh0omub].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/b5ceb5af-2318-48e7-9c83-aec81a0ff0e2.mp4" - }, - { - "id": "b6075589-21ce-4ea1-8738-c1854d5d9122", - "created_date": "2025-01-19 13:42:39.706398", - "last_modified_date": "2025-01-19 13:42:39.706404", - "version": 0, - "url": "https://ge.xhamster.com/videos/step-mom-and-step-aunt-want-to-get-pregnant-at-the-same-time-by-fucking-stepson-together-xhrMvBr", - "review": 0, - "should_download": 0, - "title": "Stiefmutter und stieftante wollen gleichzeitig schwanger werden, indem sie zusammen stiefsohn ficken | xHamster", - "file_name": "Stiefmutter und stieftante wollen gleichzeitig schwanger werden, indem sie zusammen stiefsohn ficken [xhrMvBr].mp4", - "path": null, - "cloud_link": null - }, - { - "id": "b62eb2f4-113b-430c-881b-0dbce5031d8d", - "created_date": "2024-07-25 07:29:44.381480", - "last_modified_date": "2024-07-25 07:29:44.381480", - "version": 0, - "url": "https://ge.xhamster.com/videos/mydirtyhobby-sauna-hot-threesome-xh45mq7", - "review": 0, - "should_download": 0, - "title": "Mydirtyhobby - hei\u00dfer Dreier in der Sauna | xHamster", - "file_name": "Mydirtyhobby - hei\u00dfer Dreier in der Sauna [xh45mq7].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/b62eb2f4-113b-430c-881b-0dbce5031d8d.mp4" - }, - { - "id": "b63149eb-b41b-457e-b973-46ac84f5a40e", - "created_date": "2025-01-16 19:59:37.074074", - "last_modified_date": "2025-01-16 19:59:37.074080", - "version": 0, - "url": "https://ge.xhamster.com/videos/orgies-were-different-back-then-xhVLdmF", - "review": 0, - "should_download": 0, - "title": "Orgien waren damals anders | xHamster", - "file_name": "Orgien waren damals anders [xhVLdmF].mp4", - "path": null, - "cloud_link": "/data/media/b63149eb-b41b-457e-b973-46ac84f5a40e.mp4" - }, - { - "id": "b6ce820b-5401-46ae-9aed-b0cbe332f4f5", - "created_date": "2024-08-09 20:40:06.114521", - "last_modified_date": "2024-08-16 10:31:31.551000", - "version": 1, - "url": "https://ge.xhamster.com/videos/step-son-step-mom-fuck-around-with-step-cousin-and-stepaunt-summer-vacation-taboo-family-orgy-xhu12nz", - "review": 0, - "should_download": 0, - "title": "Stiefsohn & stiefmutter ficken mit stiefcousin und stieftante - sommerferien tabu familienorgie | xHamster", - "file_name": "Stiefsohn & stiefmutter ficken mit stiefcousin und stieftante - sommerferien tabu familienorgie [xhu12nz].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/b6ce820b-5401-46ae-9aed-b0cbe332f4f5.mp4" - }, - { - "id": "b6cf9fba-a589-4cef-b380-4a49633f9e21", - "created_date": "2024-07-25 07:29:46.137338", - "last_modified_date": "2024-07-25 07:29:46.137338", - "version": 0, - "url": "https://ge.xhamster.com/videos/seafaring-teen-facialized-after-foursome-boat-banging-10295098", - "review": 0, - "should_download": 0, - "title": "Seafaring Teen nach dem Vierer-Boot-H\u00e4mmern ins Gesicht gespritzt | xHamster", - "file_name": "Seafaring Teen nach dem Vierer-Boot-H\u00e4mmern ins Gesicht gespritzt [10295098].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b6cf9fba-a589-4cef-b380-4a49633f9e21.mp4" - }, - { - "id": "b70aa94b-66bf-4777-bfc8-36efb530b874", - "created_date": "2024-07-25 07:29:44.273751", - "last_modified_date": "2024-07-25 07:29:44.273751", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-pervers-1997-xhWlXY8", - "review": 0, - "should_download": 0, - "title": "Perverse familie (1997) | xHamster", - "file_name": "Perverse familie (1997) [xhWlXY8].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/b70aa94b-66bf-4777-bfc8-36efb530b874.mp4" - }, - { - "id": "b769d481-0c54-41e8-a263-85aae2789fef", - "created_date": "2024-07-25 07:29:45.941196", - "last_modified_date": "2024-07-25 07:29:45.941196", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-taboo-german-wilder-fick-mit-notgeiler-verwandtschaft-xhD3DhB", - "review": 0, - "should_download": 0, - "title": "Family Taboo German - Wilder Fick Mit Notgeiler Verwandtschaft | xHamster", - "file_name": "FAMILY TABOO GERMAN - Wilder fick mit notgeiler Verwandtschaft [xhD3DhB].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b769d481-0c54-41e8-a263-85aae2789fef.mp4" - }, - { - "id": "b76d3793-ce72-4ed5-9eb8-c53f28dec4c2", - "created_date": "2024-07-25 07:29:45.274435", - "last_modified_date": "2024-07-25 07:29:45.274435", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsiblingscaught-tricking-step-sis-into-a-sexual-treat-12694513", - "review": 0, - "should_download": 0, - "title": "Stepsiblingscaught - Stiefschwester zu einer sexuellen Belohnung tricksen | xHamster", - "file_name": "Stepsiblingscaught - Stiefschwester zu einer sexuellen Belohnung tricksen [12694513].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b76d3793-ce72-4ed5-9eb8-c53f28dec4c2.mp4" - }, - { - "id": "b781a759-92d3-4ba1-bcc4-a0f0c8099f8e", - "created_date": "2024-07-25 07:29:46.722385", - "last_modified_date": "2024-07-25 07:29:46.722385", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-hit-1975-11241108", - "review": 0, - "should_download": 0, - "title": "Der Hit (1975) | xHamster", - "file_name": "Der Hit (1975) [11241108].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b781a759-92d3-4ba1-bcc4-a0f0c8099f8e.mp4" - }, - { - "id": "b78f9f1a-8848-495d-bb88-77a93427f7db", - "created_date": "2024-07-25 07:29:46.942136", - "last_modified_date": "2024-07-25 07:29:46.942136", - "version": 0, - "url": "https://ge.xhamster.com/videos/teen-with-ponytails-gets-aroused-from-touching-herself-xhap1aV", - "review": 0, - "should_download": 0, - "title": "Teen mit Pferdeschw\u00e4nzen wird erregt, sich selbst zu ber\u00fchren | xHamster", - "file_name": "Teen mit Pferdeschw\u00e4nzen wird erregt, sich selbst zu ber\u00fchren [xhap1aV].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b78f9f1a-8848-495d-bb88-77a93427f7db.mp4" - }, - { - "id": "b7950486-af5b-4233-a93a-15645144dd76", - "created_date": "2024-07-25 07:29:47.186747", - "last_modified_date": "2024-07-25 07:29:47.186747", - "version": 0, - "url": "https://ge.xhamster.com/videos/i-accidentally-sent-my-stepbrother-nudes-xhctN9H", - "review": 0, - "should_download": 0, - "title": "Ich habe versehentlich meinem Stiefbruder Nacktfotos geschickt | xHamster", - "file_name": "Ich habe versehentlich meinem Stiefbruder Nacktfotos geschickt [xhctN9H].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b7950486-af5b-4233-a93a-15645144dd76.mp4" - }, - { - "id": "b7aa45e4-0b5f-4c34-a18f-33a34b877940", - "created_date": "2024-07-25 07:29:46.522749", - "last_modified_date": "2024-07-25 07:29:46.522749", - "version": 0, - "url": "https://ge.xhamster.com/videos/teacher-fucked-by-two-students-2776488", - "review": 0, - "should_download": 0, - "title": "Lehrerin von zwei Sch\u00fclern gefickt | xHamster", - "file_name": "Lehrerin von zwei Sch\u00fclern gefickt [2776488].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b7aa45e4-0b5f-4c34-a18f-33a34b877940.mp4" - }, - { - "id": "b7c661de-949e-414b-af28-696668755ab7", - "created_date": "2024-07-25 07:29:47.476982", - "last_modified_date": "2024-07-25 07:29:47.476982", - "version": 0, - "url": "https://ge.xhamster.com/videos/eine-verdammt-heisse-braut-2-1989-9356438", - "review": 0, - "should_download": 0, - "title": "Eine Verdammt Heisse Braut 2 1989, Free Porn 5e | xHamster", - "file_name": "Eine verdammt heisse Braut 2 (1989) [9356438].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b7c661de-949e-414b-af28-696668755ab7.mp4" - }, - { - "id": "b7d36806-d8b8-49a5-95bb-7d8bb9200ecb", - "created_date": "2024-07-25 07:29:44.296342", - "last_modified_date": "2024-07-25 07:29:44.296342", - "version": 0, - "url": "https://ge.xhamster.com/videos/extreme-luck-1991-xhyvum3", - "review": 0, - "should_download": 0, - "title": "- Extreme Luck 1991: In German Porn Video 2d | xHamster", - "file_name": "- Extreme Luck (1991) [xhyvum3].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b7d36806-d8b8-49a5-95bb-7d8bb9200ecb.mp4" - }, - { - "id": "b7dc8b17-0b0f-4b4f-90ae-581f528edf1e", - "created_date": "2024-12-29 23:53:27.900216", - "last_modified_date": "2024-12-29 23:53:27.900216", - "version": 0, - "url": "https://ge.xhamster.com/videos/creampie-for-his-young-stepsis-14605707", - "review": 0, - "should_download": 0, - "title": "Creampie f\u00fcr seine junge Stiefschwester | xHamster", - "file_name": "Creampie f\u00fcr seine junge Stiefschwester [14605707].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/b7dc8b17-0b0f-4b4f-90ae-581f528edf1e.mp4" - }, - { - "id": "b7e9854d-5d76-4525-8fe5-1447312c1b48", - "created_date": "2024-07-25 07:29:47.137774", - "last_modified_date": "2024-07-25 07:29:47.137774", - "version": 0, - "url": "https://ge.xhamster.com/videos/vintage-german-virgin-maid-8509090", - "review": 0, - "should_download": 0, - "title": "Retro deutsches Retro Zimmerm\u00e4dchen | xHamster", - "file_name": "Retro deutsches Retro Zimmerm\u00e4dchen [8509090].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b7e9854d-5d76-4525-8fe5-1447312c1b48.mp4" - }, - { - "id": "b8363db8-8f4a-49bd-aa5e-873657a4d8a9", - "created_date": "2024-12-29 23:53:27.899146", - "last_modified_date": "2024-12-29 23:53:27.899146", - "version": 0, - "url": "https://ge.xhamster.com/videos/orgy-13634510", - "review": 0, - "should_download": 0, - "title": "Orgie | xHamster", - "file_name": "Orgie [13634510].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/b8363db8-8f4a-49bd-aa5e-873657a4d8a9.mp4" - }, - { - "id": "b8737f99-8c45-4d30-bbb7-c3477e90162e", - "created_date": "2024-07-25 07:29:47.945971", - "last_modified_date": "2024-07-25 07:29:47.945971", - "version": 0, - "url": "https://ge.xhamster.com/videos/orgien-auf-schloss-pimmelhof-1990s-german-sound-full-dvd-xhIcqJb", - "review": 0, - "should_download": 0, - "title": "Orgien Auf Schloss Pimmelhof 1990s German Sound Full Dvd | xHamster", - "file_name": "Orgien auf Schloss Pimmelhof (1990s, German sound, full DVD) [xhIcqJb].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b8737f99-8c45-4d30-bbb7-c3477e90162e.mp4" - }, - { - "id": "b8ae4604-ea5f-4236-ab0d-75c8b3f22a43", - "created_date": "2024-07-25 07:29:47.299815", - "last_modified_date": "2024-07-25 07:29:47.299815", - "version": 0, - "url": "https://ge.xhamster.com/videos/want-to-fuck-your-step-aunt-in-ass-family-fantasy-xhkN74e", - "review": 0, - "should_download": 0, - "title": "willst du deine stieftante in den arsch ficken? - Familienfantasie | xHamster", - "file_name": "willst du deine stieftante in den arsch ficken\uff1f - Familienfantasie [xhkN74e].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b8ae4604-ea5f-4236-ab0d-75c8b3f22a43.mp4" - }, - { - "id": "b8f54c89-e185-4fde-bf70-1f0c147a245f", - "created_date": "2024-07-25 07:29:46.277815", - "last_modified_date": "2024-07-25 07:29:46.277815", - "version": 0, - "url": "https://ge.xhamster.com/videos/fucking-mother-in-law-is-fun-xhtXUXG", - "review": 0, - "should_download": 0, - "title": "SCHWIEGERMUTTER FICKEN MACHT SPASS !!! | xHamster", - "file_name": "SCHWIEGERMUTTER FICKEN MACHT SPASS !!! [xhtXUXG].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b8f54c89-e185-4fde-bf70-1f0c147a245f.mp4" - }, - { - "id": "b9284d00-2673-4d03-9a16-923f14d30394", - "created_date": "2024-12-29 23:53:27.958369", - "last_modified_date": "2024-12-29 23:53:27.958369", - "version": 0, - "url": "https://ge.xhamster.com/videos/lezione-di-sesso-1980-italy-dominique-saint-claire-dvd-xh1Ldzu", - "review": 0, - "should_download": 0, - "title": "Lezione Di Sesso 1980 Italy Dominique Saint Claire Dvd | xHamster", - "file_name": "Lezione di sesso (1980, Italy, Dominique Saint Claire, DVD) [xh1Ldzu].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/b9284d00-2673-4d03-9a16-923f14d30394.mp4" - }, - { - "id": "b9364d25-bcd5-401c-9743-cbb1d4b34e90", - "created_date": "2024-07-25 07:29:46.964078", - "last_modified_date": "2024-07-25 07:29:46.964078", - "version": 0, - "url": "https://ge.xhamster.com/videos/karina-grand-dp-3855463", - "review": 0, - "should_download": 0, - "title": "Karina Grand DP: Free Threesome Porn Video ee | xHamster", - "file_name": "Karina Grand DP [3855463].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b9364d25-bcd5-401c-9743-cbb1d4b34e90.mp4" - }, - { - "id": "b938bdba-02a5-40ff-8677-6a8c370bc213", - "created_date": "2024-07-25 07:29:45.237614", - "last_modified_date": "2024-07-25 07:29:45.237614", - "version": 0, - "url": "https://ge.xhamster.com/videos/bisex-stepmom-sometimes-needs-cock-13735412", - "review": 0, - "should_download": 0, - "title": "Bisex-Stiefmutter braucht manchmal Schwanz | xHamster", - "file_name": "Bisex-Stiefmutter braucht manchmal Schwanz [13735412].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b938bdba-02a5-40ff-8677-6a8c370bc213.mp4" - }, - { - "id": "b97a109a-17a2-4fff-a6a8-f5b15cf78382", - "created_date": "2024-07-25 07:29:46.289893", - "last_modified_date": "2024-07-25 07:29:46.289893", - "version": 0, - "url": "https://ge.xhamster.com/videos/orgy-at-the-river-14429799", - "review": 0, - "should_download": 0, - "title": "Orgy at the river | xHamster", - "file_name": "Orgy at the river [14429799].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b97a109a-17a2-4fff-a6a8-f5b15cf78382.mp4" - }, - { - "id": "b9ac8b59-4bb3-4633-afc6-5e64bcdbcfb4", - "created_date": "2024-07-25 07:29:46.652308", - "last_modified_date": "2024-07-25 07:29:46.652308", - "version": 0, - "url": "https://ge.xhamster.com/videos/horny-stepdaughter-coco-lovelock-tries-stepdads-cock-in-rough-threesome-with-milf-kenzie-taylor-xhbFshz", - "review": 0, - "should_download": 0, - "title": "Die geile stieftochter Coco LoveLock versucht den schwanz des stiefvaters in grobem dreier mit MILF Kenzie Taylor | xHamster", - "file_name": "Die geile stieftochter Coco LoveLock versucht den schwanz des stiefvaters in grobem dreier mit MILF Kenzie Taylor [xhbFshz].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b9ac8b59-4bb3-4633-afc6-5e64bcdbcfb4.mp4" - }, - { - "id": "b9eecdd4-88c7-4d0e-8313-762a5aa2ea44", - "created_date": "2024-07-25 07:29:45.487885", - "last_modified_date": "2024-07-25 07:29:45.487885", - "version": 0, - "url": "https://ge.xhamster.com/videos/ein-sommer-im-zeichen-von-pussy-full-movie-xh4Jaxj", - "review": 0, - "should_download": 0, - "title": "Ein Sommer Im Zeichen Von Pussy Full Movie: Free Porn c8 | xHamster", - "file_name": "Ein Sommer im Zeichen von Pussy (Full Movie) [xh4Jaxj].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/b9eecdd4-88c7-4d0e-8313-762a5aa2ea44.mp4" - }, - { - "id": "b9fad5b8-456b-4d60-aa8d-dff9d90a48b2", - "created_date": "2024-07-25 07:29:44.879492", - "last_modified_date": "2024-07-25 07:29:44.879492", - "version": 0, - "url": "https://ge.xhamster.com/videos/loving-family-2-xhNdokk", - "review": 0, - "should_download": 0, - "title": "Liebende Familie 2 | xHamster", - "file_name": "Liebende Familie 2 [xhNdokk].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/b9fad5b8-456b-4d60-aa8d-dff9d90a48b2.mp4" - }, - { - "id": "ba081073-8c6e-4309-9041-23f34433c7f7", - "created_date": "2024-07-25 07:29:47.986349", - "last_modified_date": "2024-07-25 07:29:47.986349", - "version": 0, - "url": "https://ge.xhamster.com/videos/lust-weekend-1988-us-sharon-mitchell-full-video-dvdrip-xhTkKEE", - "review": 0, - "should_download": 0, - "title": "Lust-Wochenende (1988, wir, Sharon Mitchell, komplettes Video, dvdrip) | xHamster", - "file_name": "Lust-Wochenende (1988, wir, Sharon Mitchell, komplettes Video, dvdrip) [xhTkKEE].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ba081073-8c6e-4309-9041-23f34433c7f7.mp4" - }, - { - "id": "ba45cb1e-7ecc-41ce-909b-043520bf961e", - "created_date": "2024-10-21 15:08:43.543606", - "last_modified_date": "2024-10-21 16:31:44.107000", - "version": 1, - "url": "https://ge.xhamster.com/videos/secret-ritual-at-the-office-7634370", - "review": 0, - "should_download": 0, - "title": "Geheimes Ritual im B\u00fcro | xHamster", - "file_name": "Geheimes Ritual im B\u00fcro [7634370].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/ba45cb1e-7ecc-41ce-909b-043520bf961e.mp4" - }, - { - "id": "ba5ae46f-577c-46fa-9f9f-bac77dc8d528", - "created_date": "2024-07-25 07:29:45.862018", - "last_modified_date": "2024-07-25 07:29:45.862018", - "version": 0, - "url": "https://ge.xhamster.com/videos/fick-academy-aka-college-a-tout-faire-1977-9452790", - "review": 0, - "should_download": 0, - "title": "Fick Academy, College auch bekannt als A tout faire (1977) | xHamster", - "file_name": "Fick Academy, College auch bekannt als A tout faire (1977) [9452790].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ba5ae46f-577c-46fa-9f9f-bac77dc8d528.mp4" - }, - { - "id": "bb46f4fa-a0ae-46ca-963f-4effd2e16e04", - "created_date": "2024-07-25 07:29:47.337305", - "last_modified_date": "2024-07-25 07:29:47.337305", - "version": 0, - "url": "https://ge.xhamster.com/videos/eroticax-teen-tries-out-swinger-lifestyle-xhjtwpg", - "review": 0, - "should_download": 0, - "title": "Eroticax - ein Teenager probiert den Swinger-Lifestyle aus | xHamster", - "file_name": "Eroticax - ein Teenager probiert den Swinger-Lifestyle aus [xhjtwpg].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/bb46f4fa-a0ae-46ca-963f-4effd2e16e04.mp4" - }, - { - "id": "bb472ff0-a885-44e0-bdca-d340364400a6", - "created_date": "2024-07-25 07:29:46.260157", - "last_modified_date": "2024-07-25 07:29:46.260157", - "version": 0, - "url": "https://ge.xhamster.com/videos/stp7-a-family-that-love-their-weekly-get-together-8246579", - "review": 0, - "should_download": 0, - "title": "Stp7 eine Familie, die ihr w\u00f6chentliches Treffen liebt! | xHamster", - "file_name": "Stp7 eine Familie, die ihr w\u00f6chentliches Treffen liebt! [8246579].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/bb472ff0-a885-44e0-bdca-d340364400a6.mp4" - }, - { - "id": "bb583b0c-2c51-4d42-b10f-bee5998ea257", - "created_date": "2024-07-25 07:29:48.044685", - "last_modified_date": "2024-07-25 07:29:48.044685", - "version": 0, - "url": "https://ge.xhamster.com/videos/kelly-the-coed-9-xhLJndr", - "review": 0, - "should_download": 0, - "title": "Kelly the Coed # 9 | xHamster", - "file_name": "Kelly the Coed # 9 [xhLJndr].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/bb583b0c-2c51-4d42-b10f-bee5998ea257.mp4" - }, - { - "id": "bbf86b63-0d9f-44dd-afa4-af6a16313368", - "created_date": "2024-07-25 07:29:46.610826", - "last_modified_date": "2024-07-25 07:29:46.610826", - "version": 0, - "url": "https://ge.xhamster.com/videos/gruppensex-total-4-1992-xhR4K42", - "review": 0, - "should_download": 0, - "title": "Gruppensex Total 4 1992, Free In English Porn 9d | xHamster", - "file_name": "Gruppensex Total 4 (1992) [xhR4K42].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/bbf86b63-0d9f-44dd-afa4-af6a16313368.mp4" - }, - { - "id": "bca9c388-353c-463d-a50c-f3ca469ca2f1", - "created_date": "2024-07-25 07:29:47.926842", - "last_modified_date": "2024-07-25 07:29:47.926842", - "version": 0, - "url": "https://ge.xhamster.com/videos/tushy-shy-avery-seduces-has-anal-with-her-longtime-crush-xhfaV01", - "review": 0, - "should_download": 0, - "title": "Tushy, sch\u00fcchtern, Avery verf\u00fchrt & hat Anal mit ihrem langj\u00e4hrigen Schwarm | xHamster", - "file_name": "Tushy, sch\u00fcchtern, Avery verf\u00fchrt & hat Anal mit ihrem langj\u00e4hrigen Schwarm [xhfaV01].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/bca9c388-353c-463d-a50c-f3ca469ca2f1.mp4" - }, - { - "id": "bce19187-cf3b-472c-8abd-9e2fe6964977", - "created_date": "2024-07-25 07:29:47.624325", - "last_modified_date": "2024-07-25 07:29:47.624325", - "version": 0, - "url": "https://ge.xhamster.com/videos/das-dorf-der-feuchten-fotzen-1975-with-vivian-smith-6014330", - "review": 0, - "should_download": 0, - "title": "Das Dorf Der Feuchten Fotzen 1975 with Vivian Smith | xHamster", - "file_name": "Das Dorf der feuchten Fotzen (1975) with Vivian Smith [6014330].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/bce19187-cf3b-472c-8abd-9e2fe6964977.mp4" - }, - { - "id": "bd00ade2-4198-4de5-8256-9fc1cd188e9e", - "created_date": "2024-07-25 07:29:45.105513", - "last_modified_date": "2024-07-25 07:29:45.105513", - "version": 0, - "url": "https://ge.xhamster.com/videos/redhead-casted-british-cleaning-the-pipes-7409931", - "review": 0, - "should_download": 0, - "title": "Rothaarige bespritzt Briten beim Putzen der Rohre | xHamster", - "file_name": "Rothaarige bespritzt Briten beim Putzen der Rohre [7409931].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/bd00ade2-4198-4de5-8256-9fc1cd188e9e.mp4" - }, - { - "id": "bd25ae1e-4cd2-4c24-9fa3-438d548010e3", - "created_date": "2024-07-25 07:29:47.024744", - "last_modified_date": "2024-07-25 07:29:47.024744", - "version": 0, - "url": "https://ge.xhamster.com/videos/open-marriage-couple-pick-up-teen-for-wet-juices-threesome-xhHofg1", - "review": 0, - "should_download": 0, - "title": "Offene Ehepaare heben Teen f\u00fcr Dreier mit nassen S\u00e4ften auf | xHamster", - "file_name": "Offene Ehepaare heben Teen f\u00fcr Dreier mit nassen S\u00e4ften auf [xhHofg1].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/bd25ae1e-4cd2-4c24-9fa3-438d548010e3.mp4" - }, - { - "id": "bd358689-16c1-4d8d-b762-8e0e5f4846b6", - "created_date": "2024-10-07 20:47:56.430267", - "last_modified_date": "2024-10-21 16:31:48.213000", - "version": 1, - "url": "https://ge.xhamster.com/videos/nach-der-hochzeit-im-hallenbad-gefickt-2231403", - "review": 0, - "should_download": 0, - "title": "Nach Der Hochzeit Im Hallenbad Gefickt, Porn 2a | xHamster", - "file_name": "Nach der Hochzeit im Hallenbad gefickt [2231403].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/bd358689-16c1-4d8d-b762-8e0e5f4846b6.mp4" - }, - { - "id": "bd389f6a-5b0c-4707-a7dd-a12f5fb6e162", - "created_date": "2024-07-25 07:29:45.195869", - "last_modified_date": "2024-07-25 07:29:45.195869", - "version": 0, - "url": "https://ge.xhamster.com/videos/married-couple-talk-younger-neighbor-into-a-threesome-12233465", - "review": 0, - "should_download": 0, - "title": "Ehepaar \u00fcberredet j\u00fcngeren Nachbarn zu einem Dreier | xHamster", - "file_name": "Ehepaar \u00fcberredet j\u00fcngeren Nachbarn zu einem Dreier [12233465].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/bd389f6a-5b0c-4707-a7dd-a12f5fb6e162.mp4" - }, - { - "id": "bd48b9cf-c4e5-487f-8750-0ccfee4926b9", - "created_date": "2024-07-25 07:29:47.722093", - "last_modified_date": "2024-07-25 07:29:47.722093", - "version": 0, - "url": "https://ge.xhamster.com/videos/entangled-1993-xhjPDFN", - "review": 0, - "should_download": 0, - "title": "Entangled (1993) | xHamster", - "file_name": "Entangled (1993) [xhjPDFN].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/bd48b9cf-c4e5-487f-8750-0ccfee4926b9.mp4" - }, - { - "id": "bd903746-772b-4bd0-9f34-6d3ddb7c40b2", - "created_date": "2024-07-25 07:29:47.264956", - "last_modified_date": "2024-07-25 07:29:47.264956", - "version": 0, - "url": "https://ge.xhamster.com/videos/secretary-inside-1993-xh1VUlX", - "review": 0, - "should_download": 0, - "title": "Secretary Inside 1993, Free European Porn 06 | xHamster", - "file_name": "Secretary Inside (1993) [xh1VUlX].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/bd903746-772b-4bd0-9f34-6d3ddb7c40b2.mp4" - }, - { - "id": "bd922d44-9b45-48e5-b09e-bc9946e5b6a2", - "created_date": "2024-07-25 07:29:44.707030", - "last_modified_date": "2024-07-25 07:29:44.707030", - "version": 0, - "url": "https://ge.xhamster.com/videos/18-and-confused-2-xhrowbj", - "review": 0, - "should_download": 0, - "title": "18 und verwirrt # 2 | xHamster", - "file_name": "18 und verwirrt # 2 [xhrowbj].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/bd922d44-9b45-48e5-b09e-bc9946e5b6a2.mp4" - }, - { - "id": "be12b4f6-2af5-4c04-99aa-d35fc71bd0cd", - "created_date": "2024-07-25 07:29:46.306001", - "last_modified_date": "2024-07-25 07:29:46.306001", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-sex-exam-10511897", - "review": 0, - "should_download": 0, - "title": "Die Sex-Untersuchung | xHamster", - "file_name": "Die Sex-Untersuchung [10511897].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/be12b4f6-2af5-4c04-99aa-d35fc71bd0cd.mp4" - }, - { - "id": "be14b4ea-59a0-4fa7-9112-a2b65d75d455", - "created_date": "2024-08-28 23:21:54.366218", - "last_modified_date": "2024-08-28 23:21:54.366218", - "version": 0, - "url": "https://ge.xhamster.com/videos/forbidden-sex-games-1986-xhjfy3w", - "review": 0, - "should_download": 0, - "title": "Verbotene Sex-Spiele (1986) | xHamster", - "file_name": "Verbotene Sex-Spiele (1986) [xhjfy3w].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/be14b4ea-59a0-4fa7-9112-a2b65d75d455.mp4" - }, - { - "id": "be476378-45a3-4101-9894-2555bc5722c1", - "created_date": "2024-07-25 07:29:45.004740", - "last_modified_date": "2024-07-25 07:29:45.004740", - "version": 0, - "url": "https://ge.xhamster.com/videos/milf-katrina-colt-says-you-just-need-a-more-experienced-woman-to-help-you-s20-e2-xhkkZiG", - "review": 0, - "should_download": 0, - "title": "Milf Katrina Colt sagt, \"Sie brauchen nur eine erfahrenere Frau, um ihnen zu helfen\" - S20: E2 | xHamster", - "file_name": "Milf Katrina Colt sagt, \uff02Sie brauchen nur eine erfahrenere Frau, um ihnen zu helfen\uff02 - S20\uff1a E2 [xhkkZiG].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/be476378-45a3-4101-9894-2555bc5722c1.mp4" - }, - { - "id": "be4b3710-5bca-4cf8-bb85-568f78bfec20", - "created_date": "2024-07-25 07:29:46.293811", - "last_modified_date": "2024-07-25 07:29:46.293811", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsister-decided-to-have-sex-with-stepbrother-while-parents-are-not-at-home-syndicete-xhb88V7", - "review": 0, - "should_download": 0, - "title": "Stiefschwester beschloss, Sex mit Stiefbruder zu haben, w\u00e4hrend die Eltern nicht zu Hause sind - Syndicete | xHamster", - "file_name": "Stiefschwester beschloss, Sex mit Stiefbruder zu haben, w\u00e4hrend die Eltern nicht zu Hause sind - Syndicete [xhb88V7].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/be4b3710-5bca-4cf8-bb85-568f78bfec20.mp4" - }, - { - "id": "be52ce42-dd54-49f0-b599-c7136b54582d", - "created_date": "2024-07-25 07:29:45.021863", - "last_modified_date": "2024-07-25 07:29:45.021863", - "version": 0, - "url": "https://ge.xhamster.com/videos/bratty-sis-step-brother-and-sister-get-caught-fucking-s3-e-8770848", - "review": 0, - "should_download": 0, - "title": "Bratty sis - Stiefbruder und Schwester werden beim Ficken bei s3: e erwischt | xHamster", - "file_name": "Bratty sis - Stiefbruder und Schwester werden beim Ficken bei s3\uff1a e erwischt [8770848].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/be52ce42-dd54-49f0-b599-c7136b54582d.mp4" - }, - { - "id": "be91eba0-0f6f-4e6e-a9f6-13221f8f26e4", - "created_date": "2024-07-25 07:29:44.783695", - "last_modified_date": "2024-07-25 07:29:44.783695", - "version": 0, - "url": "https://ge.xhamster.com/videos/momsteachsex-mom-and-step-son-share-bed-and-fuck-s7-e3-8735140", - "review": 0, - "should_download": 0, - "title": "Momsteachsex - Mutter und Stiefsohn teilen sich das Bett und ficken s7: e3 | xHamster", - "file_name": "Momsteachsex - Mutter und Stiefsohn teilen sich das Bett und ficken s7\uff1a e3 [8735140].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/be91eba0-0f6f-4e6e-a9f6-13221f8f26e4.mp4" - }, - { - "id": "bf0d9c52-fb5f-4035-bee2-eee3d3661849", - "created_date": "2024-10-21 15:08:43.555609", - "last_modified_date": "2024-10-21 16:31:52.522000", - "version": 1, - "url": "https://ge.xhamster.com/videos/night-at-the-roxburys-10064260", - "review": 0, - "should_download": 0, - "title": "Nacht bei den Roxbury's | xHamster", - "file_name": "Nacht bei den Roxbury's [10064260].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/bf0d9c52-fb5f-4035-bee2-eee3d3661849.mp4" - }, - { - "id": "bfa37f78-a5ac-47b6-92b0-22c01984482a", - "created_date": "2024-07-25 07:29:45.796804", - "last_modified_date": "2024-07-25 07:29:45.796804", - "version": 0, - "url": "https://ge.xhamster.com/videos/american-college-xxx-vol-8-original-version-in-hd-xhaG41o", - "review": 0, - "should_download": 0, - "title": "American College xxx - vol (8) - (Originalversion in hd) | xHamster", - "file_name": "American College xxx - vol (8) - (Originalversion in hd) [xhaG41o].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/bfa37f78-a5ac-47b6-92b0-22c01984482a.mp4" - }, - { - "id": "c027e260-1312-43d5-baa2-09e75742d6d6", - "created_date": "2025-01-19 13:42:27.340286", - "last_modified_date": "2025-01-19 13:42:27.340293", - "version": 0, - "url": "https://ge.xhamster.com/videos/best-friends-you-give-a-kiss-and-the-cock-at-the-same-time-xhMbGyP", - "review": 0, - "should_download": 0, - "title": "besten freunden gibt man ein K\u00fcsschen und den Schwanz gleich dazu | xHamster", - "file_name": "besten freunden gibt man ein K\u00fcsschen und den Schwanz gleich dazu [xhMbGyP].mp4", - "path": null, - "cloud_link": null - }, - { - "id": "c117fa7d-e04d-4950-b85b-31337b30a17f", - "created_date": "2024-07-25 07:29:45.828603", - "last_modified_date": "2024-07-25 07:29:45.828603", - "version": 0, - "url": "https://ge.xhamster.com/videos/haley-spades-convinces-stepbro-we-should-role-play-together-s8-e4-xhl9dJP", - "review": 0, - "should_download": 0, - "title": "Haley Spades \u00fcberzeugt Stiefbruder: \"Wir sollten zusammen Rollenspiele machen!\" - s8: e4 | xHamster", - "file_name": "Haley Spades \u00fcberzeugt Stiefbruder\uff1a \uff02Wir sollten zusammen Rollenspiele machen!\uff02 - s8\uff1a e4 [xhl9dJP].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c117fa7d-e04d-4950-b85b-31337b30a17f.mp4" - }, - { - "id": "c12ba4e3-4dc0-481f-837f-04ad821a14cb", - "created_date": "2024-07-25 07:29:44.906642", - "last_modified_date": "2024-07-25 07:29:44.906642", - "version": 0, - "url": "https://ge.xhamster.com/videos/der-hausmeister-full-movie-xhpC5HPO", - "review": 0, - "should_download": 0, - "title": "Der Hausmeister Full Movie, Free German Porn d3 | xHamster", - "file_name": "Der Hausmeister (Full Movie) [xhpC5HPO].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c12ba4e3-4dc0-481f-837f-04ad821a14cb.mp4" - }, - { - "id": "c16be31d-84c3-424f-8be8-7794ce878e3d", - "created_date": "2024-07-25 07:29:45.312891", - "last_modified_date": "2024-07-25 07:29:45.312891", - "version": 0, - "url": "https://ge.xhamster.com/videos/2-cocks-and-sperm-instead-of-sauna-infusion-xhpxej2", - "review": 0, - "should_download": 0, - "title": "2 Schw\u00e4nze und Sperma statt Sauna Aufguss | xHamster", - "file_name": "2 Schw\u00e4nze und Sperma statt Sauna Aufguss [xhpxej2].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c16be31d-84c3-424f-8be8-7794ce878e3d.mp4" - }, - { - "id": "c1bb1c9c-d4b5-469b-85d3-2e2ebc5b73f4", - "created_date": "2024-07-25 07:29:44.265675", - "last_modified_date": "2024-07-25 07:29:44.265675", - "version": 0, - "url": "https://ge.xhamster.com/videos/working-girls-1983-12151003", - "review": 0, - "should_download": 0, - "title": "Working Girls (1983) | xHamster", - "file_name": "Working Girls (1983) [12151003].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c1bb1c9c-d4b5-469b-85d3-2e2ebc5b73f4.mp4" - }, - { - "id": "c2236975-eb51-4bb0-b594-386b52e9f7fe", - "created_date": "2024-07-25 07:29:44.327775", - "last_modified_date": "2024-07-25 07:29:44.327775", - "version": 0, - "url": "https://ge.xhamster.com/videos/adult-time-college-boys-gangbang-bukkake-dp-pawg-heather-honey-in-public-library-full-scene-xhyoNBR", - "review": 0, - "should_download": 0, - "title": "ERWACHSENENZEIT - college-jungs GANGBANG BUKKAKE + DOPPELPENETRATION PAWG heather honey in der \u00d6FFENTLICHEN BIBLIOTHEK! GANZE SZENE | xHamster", - "file_name": "ERWACHSENENZEIT - college-jungs GANGBANG BUKKAKE + DOPPELPENETRATION PAWG heather honey in der \u00d6FFENTLICHEN BIBLIOTHEK! GANZE SZENE [xhyoNBR].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c2236975-eb51-4bb0-b594-386b52e9f7fe.mp4" - }, - { - "id": "c2296707-ff37-45ee-9ee7-abef5e33c7c1", - "created_date": "2024-07-25 07:29:47.552118", - "last_modified_date": "2024-07-25 07:29:47.552118", - "version": 0, - "url": "https://ge.xhamster.com/videos/unnatural-family-8749233", - "review": 0, - "should_download": 0, - "title": "Unnat\u00fcrliche Familie | xHamster", - "file_name": "Unnat\u00fcrliche Familie [8749233].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c2296707-ff37-45ee-9ee7-abef5e33c7c1.mp4" - }, - { - "id": "c2592a5e-6ffb-4d65-91af-d87258913aa3", - "created_date": "2024-09-11 10:23:29.175035", - "last_modified_date": "2024-10-21 16:31:57.497000", - "version": 1, - "url": "https://ge.xhamster.com/videos/surprise-youre-fucking-your-friends-hot-mom-today-xhKZypp", - "review": 0, - "should_download": 0, - "title": "\u00dcberraschung! Du fickst heute die hei\u00dfe mutter deines freundes! | xHamster", - "file_name": "\u00dcberraschung! Du fickst heute die hei\u00dfe mutter deines freundes! [xhKZypp].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/c2592a5e-6ffb-4d65-91af-d87258913aa3.mp4" - }, - { - "id": "c2a90005-ee9b-42a2-a523-eab2b433686f", - "created_date": "2024-10-14 20:33:38.257731", - "last_modified_date": "2024-10-21 16:32:01.227000", - "version": 1, - "url": "https://ge.xhamster.com/videos/the-sex-play-7452305", - "review": 0, - "should_download": 0, - "title": "Das Sexspiel | xHamster", - "file_name": "Das Sexspiel [7452305].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/c2a90005-ee9b-42a2-a523-eab2b433686f.mp4" - }, - { - "id": "c2ae2c0a-b32d-4149-ab38-e632eb198903", - "created_date": "2024-07-25 07:29:46.107013", - "last_modified_date": "2024-07-25 07:29:46.107013", - "version": 0, - "url": "https://ge.xhamster.com/videos/step-mom-and-stepson-summer-vacation-together-shiny-cock-films-xh5TMez", - "review": 0, - "should_download": 0, - "title": "Step Mom and Stepson Summer Vacation Together - Shiny Cock Films | xHamster", - "file_name": "Step Mom and Stepson Summer Vacation Together - Shiny Cock Films [xh5TMez].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c2ae2c0a-b32d-4149-ab38-e632eb198903.mp4" - }, - { - "id": "c2e86097-04c1-4191-b142-90d9295823ee", - "created_date": "2024-07-25 07:29:45.948391", - "last_modified_date": "2024-07-25 07:29:45.948391", - "version": 0, - "url": "https://ge.xhamster.com/videos/moni-und-lisa-die-sextollen-schwestern-xh4bGe6", - "review": 0, - "should_download": 0, - "title": "Moni Und Lisa - Die Sextollen Schwestern, Porn fe | xHamster", - "file_name": "Moni und Lisa - Die sextollen Schwestern [xh4bGe6].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c2e86097-04c1-4191-b142-90d9295823ee.mp4" - }, - { - "id": "c3488d7a-1684-4c41-8f83-5afd95ad7761", - "created_date": "2024-07-25 07:29:48.098123", - "last_modified_date": "2024-07-25 07:29:48.098123", - "version": 0, - "url": "https://ge.xhamster.com/videos/familystrokes-stepsiblings-get-caught-fucking-by-stepmom-9711431", - "review": 0, - "should_download": 0, - "title": "In Familystrokes werden Stiefgeschwister von ihrer Stiefmutter beim Ficken erwischt | xHamster", - "file_name": "In Familystrokes werden Stiefgeschwister von ihrer Stiefmutter beim Ficken erwischt [9711431].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c3488d7a-1684-4c41-8f83-5afd95ad7761.mp4" - }, - { - "id": "c34bc24a-16e5-42c5-9e7f-2cbba6cfc086", - "created_date": "2024-07-25 07:29:48.010298", - "last_modified_date": "2024-07-25 07:29:48.010298", - "version": 0, - "url": "https://ge.xhamster.com/videos/teen-sucks-a-couple-of-dicks-on-the-couch-and-get-fucked-xhUrdLi", - "review": 0, - "should_download": 0, - "title": "Teen lutscht ein paar Schw\u00e4nze auf der Couch und wird gefickt | xHamster", - "file_name": "Teen lutscht ein paar Schw\u00e4nze auf der Couch und wird gefickt [xhUrdLi].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c34bc24a-16e5-42c5-9e7f-2cbba6cfc086.mp4" - }, - { - "id": "c36af489-de25-48ff-a6f5-8802278f1741", - "created_date": "2024-07-25 07:29:46.319286", - "last_modified_date": "2024-07-25 07:29:46.319286", - "version": 0, - "url": "https://ge.xhamster.com/videos/frau-wollen-full-movie-xhgl60a", - "review": 0, - "should_download": 0, - "title": "Frau Wollen Full Movie, Free Story Porn Video 4a | xHamster", - "file_name": "FRAU WOLLEN (Full Movie) [xhgl60a].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c36af489-de25-48ff-a6f5-8802278f1741.mp4" - }, - { - "id": "c39d7eb2-1b7c-4bbf-a6b6-968c8408f4b5", - "created_date": "2024-12-29 23:53:27.884164", - "last_modified_date": "2024-12-31 00:52:22.782000", - "version": 1, - "url": "https://ge.xhamster.com/videos/vixen-lana-rhoades-has-sex-with-her-boss-xhH7snP", - "review": 0, - "should_download": 0, - "title": "Vixen Lana Rhoades hat Sex mit ihrem Chef | xHamster", - "file_name": "Vixen Lana Rhoades hat Sex mit ihrem Chef [xhH7snP].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/c39d7eb2-1b7c-4bbf-a6b6-968c8408f4b5.mp4" - }, - { - "id": "c40c2ee8-7e51-47dc-bf0b-52cf68ece492", - "created_date": "2024-07-25 07:29:45.821435", - "last_modified_date": "2025-01-03 11:56:42.284000", - "version": 1, - "url": "https://ge.xhamster.com/videos/im-either-getting-dick-today-or-this-weekend-its-your-choice-veronica-church-tells-stepbro-s27-e2-xhxZPTF", - "review": 0, - "should_download": 0, - "title": "\"Ich habe entweder heute oder an diesem Wochenende schwanz, es ist deine Wahl\" Veronica Church sagt stiefbruer - S27: e2 | xHamster", - "file_name": "\uff02Ich habe entweder heute oder an diesem Wochenende schwanz, es ist deine Wahl\uff02 Veronica Church sagt stiefbruer - S27\uff1a e2 [xhxZPTF].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c40c2ee8-7e51-47dc-bf0b-52cf68ece492.mp4" - }, - { - "id": "c4207adb-6d25-4b1f-87c0-dd20b144899a", - "created_date": "2024-07-25 07:29:45.556477", - "last_modified_date": "2024-07-25 07:29:45.556477", - "version": 0, - "url": "https://ge.xhamster.com/videos/if-you-want-your-wife-not-to-fuck-your-brains-out-you-have-to-fuck-her-hard-and-rough-xhTWv0M", - "review": 0, - "should_download": 0, - "title": "Wenn du willst, dass deine ehefrau dein gehirn nicht ausfickt, musst du sie hart und hart ficken | xHamster", - "file_name": "Wenn du willst, dass deine ehefrau dein gehirn nicht ausfickt, musst du sie hart und hart ficken [xhTWv0M].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c4207adb-6d25-4b1f-87c0-dd20b144899a.mp4" - }, - { - "id": "c4608d49-1955-4d42-897f-c0fb1d3950b2", - "created_date": "2024-07-25 07:29:45.963784", - "last_modified_date": "2024-07-25 07:29:45.963784", - "version": 0, - "url": "https://ge.xhamster.com/videos/3-way-porn-wet-t-shirt-at-poolside-orgy-13388985", - "review": 0, - "should_download": 0, - "title": "3-Wege-Porno - nasses T-Shirt bei Orgie am Pool | xHamster", - "file_name": "3-Wege-Porno - nasses T-Shirt bei Orgie am Pool [13388985].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c4608d49-1955-4d42-897f-c0fb1d3950b2.mp4" - }, - { - "id": "c4644225-ed5f-4d54-b64e-fdb5a6500916", - "created_date": "2024-07-25 07:29:47.406721", - "last_modified_date": "2024-07-25 07:29:47.406721", - "version": 0, - "url": "https://ge.xhamster.com/videos/alisson-and-stepdaddy-pool-fun-xhNqDRG", - "review": 0, - "should_download": 0, - "title": "Alisson und Stiefvater haben Spa\u00df im Pool | xHamster", - "file_name": "Alisson und Stiefvater haben Spa\u00df im Pool [xhNqDRG].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c4644225-ed5f-4d54-b64e-fdb5a6500916.mp4" - }, - { - "id": "c4b23136-3b2b-4894-8814-546137d3abc0", - "created_date": "2024-07-25 07:29:44.711345", - "last_modified_date": "2024-07-25 07:29:44.711345", - "version": 0, - "url": "https://ge.xhamster.com/videos/having-fun-with-hot-italian-girl-in-a-nude-beach-xhB2LmC", - "review": 0, - "should_download": 0, - "title": "Spa\u00df mit hei\u00dfem italienischem M\u00e4dchen an einem FKK-Strand haben | xHamster", - "file_name": "Spa\u00df mit hei\u00dfem italienischem M\u00e4dchen an einem FKK-Strand haben [xhB2LmC].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/c4b23136-3b2b-4894-8814-546137d3abc0.mp4" - }, - { - "id": "c519e955-e0a9-4861-b529-6ea561ca5a25", - "created_date": "2024-07-25 07:29:46.496519", - "last_modified_date": "2024-07-25 07:29:46.496519", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-12900151", - "review": 0, - "should_download": 0, - "title": "Familie | xHamster", - "file_name": "Familie [12900151].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c519e955-e0a9-4861-b529-6ea561ca5a25.mp4" - }, - { - "id": "c5277049-5d36-4e4f-a397-363c9a88f48a", - "created_date": "2024-07-25 07:29:45.181667", - "last_modified_date": "2024-07-25 07:29:45.181667", - "version": 0, - "url": "https://ge.xhamster.com/videos/teufliches-spiel-full-movie-xhGKEHm", - "review": 0, - "should_download": 0, - "title": "Teufliches Spiel Full Movie, Free German Porn da | xHamster", - "file_name": "Teufliches Spiel (Full Movie) [xhGKEHm].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c5277049-5d36-4e4f-a397-363c9a88f48a.mp4" - }, - { - "id": "c52dca76-7cdb-4a75-a2cd-bd7c6770058c", - "created_date": "2024-12-29 23:53:27.908539", - "last_modified_date": "2024-12-29 23:53:27.908539", - "version": 0, - "url": "https://ge.xhamster.com/videos/just-vintage-333-xhafqYa", - "review": 0, - "should_download": 0, - "title": "Nur Retro 333 | xHamster", - "file_name": "Nur Retro 333 [xhafqYa].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/c52dca76-7cdb-4a75-a2cd-bd7c6770058c.mp4" - }, - { - "id": "c536305b-5fac-4034-8a69-a182ef7611d8", - "created_date": "2024-07-25 07:29:47.662245", - "last_modified_date": "2024-07-25 07:29:47.662245", - "version": 0, - "url": "https://ge.xhamster.com/videos/die-geilste-familie-569190", - "review": 0, - "should_download": 0, - "title": "Die Geilste Familie: Threesome Porn Video fd | xHamster", - "file_name": "Die Geilste Familie [569190].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c536305b-5fac-4034-8a69-a182ef7611d8.mp4" - }, - { - "id": "c5616acd-c67e-4b1b-bb75-79ad307bfeb3", - "created_date": "2024-07-25 07:29:45.892189", - "last_modified_date": "2024-07-25 07:29:45.892189", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-stone-clan-part-i-1991-german-good-dvd-rip-xh9qGSC", - "review": 0, - "should_download": 0, - "title": "The Stone Clan, Teil i (1991, deutsch, guter DVD-Rip | xHamster", - "file_name": "The Stone Clan, Teil i (1991, deutsch, guter DVD-Rip [xh9qGSC].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c5616acd-c67e-4b1b-bb75-79ad307bfeb3.mp4" - }, - { - "id": "c5dc4500-a1df-48ce-91ed-76a2883eae19", - "created_date": "2024-09-24 08:11:39.007595", - "last_modified_date": "2024-10-21 16:32:07.751000", - "version": 1, - "url": "https://ge.xhamster.com/videos/freeze-and-shut-up-threesome-taboo-role-play-8910110", - "review": 0, - "should_download": 0, - "title": "Frieren Sie ein und halten Sie die Klappe! - Dreier-Tabu-Rollenspiel | xHamster", - "file_name": "Frieren Sie ein und halten Sie die Klappe! - Dreier-Tabu-Rollenspiel [8910110].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/c5dc4500-a1df-48ce-91ed-76a2883eae19.mp4" - }, - { - "id": "c5fa99a9-2153-4a40-87b5-771287968c47", - "created_date": "2024-07-25 07:29:45.453292", - "last_modified_date": "2024-07-25 07:29:45.453292", - "version": 0, - "url": "https://ge.xhamster.com/videos/die-hammergeile-fick-gemeinschaft-1554728", - "review": 0, - "should_download": 0, - "title": "Die Hammergeile Fick-gemeinschaft, Free Porn 07 | xHamster", - "file_name": "Die Hammergeile Fick-Gemeinschaft [1554728].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c5fa99a9-2153-4a40-87b5-771287968c47.mp4" - }, - { - "id": "c60ecddc-02cb-4697-8ff3-286c44577897", - "created_date": "2024-07-25 07:29:46.960582", - "last_modified_date": "2024-07-25 07:29:46.960582", - "version": 0, - "url": "https://ge.xhamster.com/videos/tushy-assistant-gets-dpd-by-boss-and-friend-8876577", - "review": 0, - "should_download": 0, - "title": "Tushy Assistentin wird von Chef und Freund doppelpenetriert | xHamster", - "file_name": "Tushy Assistentin wird von Chef und Freund doppelpenetriert [8876577].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c60ecddc-02cb-4697-8ff3-286c44577897.mp4" - }, - { - "id": "c6179760-6609-40f2-be7a-afca62a63d3d", - "created_date": "2024-10-21 15:08:43.549535", - "last_modified_date": "2024-10-21 16:32:11.651000", - "version": 1, - "url": "https://ge.xhamster.com/videos/brigitte-lahaie-big-orgy-1979-7853468", - "review": 0, - "should_download": 0, - "title": "Brigitte Lahaie, Gro\u00dfe Orgie (1979) | xHamster", - "file_name": "Brigitte Lahaie, Gro\u00dfe Orgie (1979) [7853468].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/c6179760-6609-40f2-be7a-afca62a63d3d.mp4" - }, - { - "id": "c64eab78-18bb-430c-809b-6d5e11a61a37", - "created_date": "2024-07-25 07:29:46.915186", - "last_modified_date": "2024-07-25 07:29:46.915186", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsiblingscaught-horny-sister-needs-my-huge-cock-s7-e6-10044143", - "review": 0, - "should_download": 0, - "title": "Stepsiblingscaught - die geile Schwester braucht meinen riesigen Schwanz s7: e6 | xHamster", - "file_name": "Stepsiblingscaught - die geile Schwester braucht meinen riesigen Schwanz s7\uff1a e6 [10044143].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c64eab78-18bb-430c-809b-6d5e11a61a37.mp4" - }, - { - "id": "c65c1447-7fb0-4283-8a13-3bcfbbe57449", - "created_date": "2024-07-25 07:29:44.883074", - "last_modified_date": "2024-07-25 07:29:44.883074", - "version": 0, - "url": "https://ge.xhamster.com/videos/dirty-games-14463567", - "review": 0, - "should_download": 0, - "title": "Schmutzige Spiele | xHamster", - "file_name": "Schmutzige Spiele [14463567].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c65c1447-7fb0-4283-8a13-3bcfbbe57449.mp4" - }, - { - "id": "c6f4e8c8-23ca-4743-ac79-9efbed910800", - "created_date": "2024-12-29 23:53:27.926322", - "last_modified_date": "2024-12-29 23:53:27.926322", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-mischiefs-part-4-step-mom-makes-step-daughter-have-a-hot-threesome-with-her-bf-xhvARKo", - "review": 0, - "should_download": 0, - "title": "Familien-unfug (teil 4): Stiefmutter l\u00e4sst stieftochter einen hei\u00dfen dreier mit ihrem freund haben | xHamster", - "file_name": "Familien-unfug (teil 4)\uff1a Stiefmutter l\u00e4sst stieftochter einen hei\u00dfen dreier mit ihrem freund haben [xhvARKo].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/c6f4e8c8-23ca-4743-ac79-9efbed910800.mp4" - }, - { - "id": "c6ff2c83-ef4d-4d90-901b-0dd457004cc0", - "created_date": "2024-07-25 07:29:45.245181", - "last_modified_date": "2024-07-25 07:29:45.245181", - "version": 0, - "url": "https://ge.xhamster.com/videos/a1nyc-among-the-greatest-porn-films-ever-made-11109571", - "review": 0, - "should_download": 0, - "title": "A1nyc geh\u00f6rt zu den gr\u00f6\u00dften Pornofilmen, die je gemacht wurden | xHamster", - "file_name": "A1nyc geh\u00f6rt zu den gr\u00f6\u00dften Pornofilmen, die je gemacht wurden [11109571].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c6ff2c83-ef4d-4d90-901b-0dd457004cc0.mp4" - }, - { - "id": "c70c3f02-3918-4147-b1aa-80e09e9f7584", - "created_date": "2024-07-25 07:29:48.066784", - "last_modified_date": "2024-07-25 07:29:48.066784", - "version": 0, - "url": "https://ge.xhamster.com/videos/die-fickprobe-1991-xh9cN4w", - "review": 0, - "should_download": 0, - "title": "Die Fickprobe 1991: Free European Porn Video 76 | xHamster", - "file_name": "Die Fickprobe (1991) [xh9cN4w].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c70c3f02-3918-4147-b1aa-80e09e9f7584.mp4" - }, - { - "id": "c74499f2-87e3-48ab-8421-dd38e56a83e3", - "created_date": "2025-01-16 19:59:32.105320", - "last_modified_date": "2025-01-16 19:59:32.105326", - "version": 0, - "url": "https://ge.xhamster.com/videos/we-all-want-to-take-a-look-xhCoXz7", - "review": 0, - "should_download": 0, - "title": "Wir alle wollen einen Blick darauf werfen | xHamster", - "file_name": "Wir alle wollen einen Blick darauf werfen [xhCoXz7].mp4", - "path": null, - "cloud_link": "/data/media/c74499f2-87e3-48ab-8421-dd38e56a83e3.mp4" - }, - { - "id": "c77b91e9-af0c-4788-b634-c53de948f538", - "created_date": "2024-07-25 07:29:45.483304", - "last_modified_date": "2024-07-25 07:29:45.483304", - "version": 0, - "url": "https://ge.xhamster.com/videos/carnal-olympics-1983-xhtlcdR", - "review": 0, - "should_download": 0, - "title": "Carnal Olympics (1983) | xHamster", - "file_name": "Carnal Olympics (1983) [xhtlcdR].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c77b91e9-af0c-4788-b634-c53de948f538.mp4" - }, - { - "id": "c79817d3-1440-49a8-b5a1-9eed35ae4171", - "created_date": "2024-07-25 07:29:48.059587", - "last_modified_date": "2024-07-25 07:29:48.059587", - "version": 0, - "url": "https://ge.xhamster.com/videos/college-girls-1977-9357766", - "review": 0, - "should_download": 0, - "title": "College-M\u00e4dchen (1977) | xHamster", - "file_name": "College-M\u00e4dchen (1977) [9357766].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c79817d3-1440-49a8-b5a1-9eed35ae4171.mp4" - }, - { - "id": "c79a80ec-513d-4a4d-8eee-54e779262adc", - "created_date": "2024-08-09 19:34:02.478621", - "last_modified_date": "2024-08-16 10:31:50.801000", - "version": 1, - "url": "https://ge.xhamster.com/videos/party-ends-in-threesome-couple-fucks-their-friend-french-kissing-and-cum-licking-from-ass-mff-xhlfTiS", - "review": 0, - "should_download": 0, - "title": "Party endet mit dreier, paar fickt ihren freund, franz\u00f6sisches k\u00fcssen und sperma lecken vom arsch, MFF | xHamster", - "file_name": "Party endet mit dreier, paar fickt ihren freund, franz\u00f6sisches k\u00fcssen und sperma lecken vom arsch, MFF [xhlfTiS].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/c79a80ec-513d-4a4d-8eee-54e779262adc.mp4" - }, - { - "id": "c7cb2f55-d63d-4788-ba17-2d51f27c2c18", - "created_date": "2024-10-07 20:47:56.428842", - "last_modified_date": "2024-10-21 16:32:16.535000", - "version": 1, - "url": "https://ge.xhamster.com/videos/boss-fuck-my-wife-for-my-promotion-my-gift-for-you-xhHE33q", - "review": 0, - "should_download": 0, - "title": "Chef - Ficken Sie f\u00fcr meine Bef\u00f6rderung meine Frau! Mein Geschenk f\u00fcr Sie! | xHamster", - "file_name": "Chef - Ficken Sie f\u00fcr meine Bef\u00f6rderung meine Frau! Mein Geschenk f\u00fcr Sie! [xhHE33q].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/c7cb2f55-d63d-4788-ba17-2d51f27c2c18.mp4" - }, - { - "id": "c81ac77b-802c-4396-b105-d6165a4cfeb6", - "created_date": "2024-07-25 07:29:47.783198", - "last_modified_date": "2024-07-25 07:29:47.783198", - "version": 0, - "url": "https://ge.xhamster.com/videos/der-familienfick-10899040", - "review": 0, - "should_download": 0, - "title": "Der Familienfick: Free Porn Video 60 | xHamster", - "file_name": "Der Familienfick [10899040].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c81ac77b-802c-4396-b105-d6165a4cfeb6.mp4" - }, - { - "id": "c8443a9e-e0ff-4455-96ca-fe71e966ccfe", - "created_date": "2024-08-28 23:21:54.369515", - "last_modified_date": "2024-08-28 23:21:54.369515", - "version": 0, - "url": "https://ge.xhamster.com/videos/summer-camp-xhupN1m", - "review": 0, - "should_download": 0, - "title": "Sommerlager | xHamster", - "file_name": "Sommerlager [xhupN1m].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/c8443a9e-e0ff-4455-96ca-fe71e966ccfe.mp4" - }, - { - "id": "c8542277-e729-4b41-b9fa-cad44a398304", - "created_date": "2024-07-25 07:29:47.121406", - "last_modified_date": "2024-07-25 07:29:47.121406", - "version": 0, - "url": "https://ge.xhamster.com/videos/money-hunting-1994-italy-german-dub-full-dvdrip-xhSNEW3", - "review": 0, - "should_download": 0, - "title": "Money Hunting (1994, Italien, deutscher Dub, voll, dvdrip) | xHamster", - "file_name": "Money Hunting (1994, Italien, deutscher Dub, voll, dvdrip) [xhSNEW3].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c8542277-e729-4b41-b9fa-cad44a398304.mp4" - }, - { - "id": "c8dcb75a-d21a-41c7-8c73-23caab07ac70", - "created_date": "2024-07-25 07:29:46.956824", - "last_modified_date": "2024-07-25 07:29:46.956824", - "version": 0, - "url": "https://ge.xhamster.com/videos/jeannie-full-movie-xhZ5qGu", - "review": 0, - "should_download": 0, - "title": "Jeannie (kompletter Film) | xHamster", - "file_name": "Jeannie (kompletter Film) [xhZ5qGu].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c8dcb75a-d21a-41c7-8c73-23caab07ac70.mp4" - }, - { - "id": "c936e4fe-443a-4280-aa14-bb8a894f4bb7", - "created_date": "2024-07-25 07:29:45.116257", - "last_modified_date": "2024-07-25 07:29:45.116257", - "version": 0, - "url": "https://ge.xhamster.com/videos/step-mom-loves-anal-2-9345917", - "review": 0, - "should_download": 0, - "title": "Stiefmutter liebt Anal 2 | xHamster", - "file_name": "Stiefmutter liebt Anal 2 [9345917].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c936e4fe-443a-4280-aa14-bb8a894f4bb7.mp4" - }, - { - "id": "c93d3388-ade3-47fd-ab2a-d50be364c58e", - "created_date": "2025-01-16 19:59:42.081886", - "last_modified_date": "2025-01-16 19:59:42.081892", - "version": 0, - "url": "https://ge.xhamster.com/videos/young-sex-parties-bachelorette-party-with-a-strippers-3519983", - "review": 0, - "should_download": 0, - "title": "Junge Sexpartys - Junggesellinnenabschied mit Stripperinnen | xHamster", - "file_name": "Junge Sexpartys - Junggesellinnenabschied mit Stripperinnen [3519983].mp4", - "path": null, - "cloud_link": "/data/media/c93d3388-ade3-47fd-ab2a-d50be364c58e.mp4" - }, - { - "id": "c99883ce-711b-41a9-ad3b-79cdc1847d6d", - "created_date": "2024-09-24 08:11:39.001735", - "last_modified_date": "2024-10-21 16:32:21.546000", - "version": 1, - "url": "https://ge.xhamster.com/videos/found-wifes-friend-in-bed-with-us-in-the-morning-and-fucked-her-afterparty-fmf-xh662Ps", - "review": 0, - "should_download": 0, - "title": "Hab mit uns morgens mit uns die freundin der ehefrau im bett gefunden und sie gefickt, nach der party, Fmf | xHamster", - "file_name": "Hab mit uns morgens mit uns die freundin der ehefrau im bett gefunden und sie gefickt, nach der party, Fmf [xh662Ps].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/c99883ce-711b-41a9-ad3b-79cdc1847d6d.mp4" - }, - { - "id": "c9dbef20-30ef-48c3-9c18-fd5d13aa64f0", - "created_date": "2024-07-25 07:29:45.399767", - "last_modified_date": "2024-07-25 07:29:45.399767", - "version": 0, - "url": "https://ge.xhamster.com/videos/shes-my-best-friend-of-course-i-told-her-about-your-dick-tiffany-tatum-tells-stepbro-s4-e7-xhi7epR", - "review": 0, - "should_download": 0, - "title": "\"Sie ist meine beste freundin! Nat\u00fcrlich habe ich ihr von deinem schwanz erz\u00e4hlt\", sagt tiffany tatum, stiefbruer - s4: e7 | xHamster", - "file_name": "\uff02Sie ist meine beste freundin! Nat\u00fcrlich habe ich ihr von deinem schwanz erz\u00e4hlt\uff02, sagt tiffany tatum, stiefbruer - s4\uff1a e7 [xhi7epR].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c9dbef20-30ef-48c3-9c18-fd5d13aa64f0.mp4" - }, - { - "id": "c9ea31dd-5e5f-415d-a014-1b444e3994d2", - "created_date": "2024-07-25 07:29:44.289846", - "last_modified_date": "2024-07-25 07:29:44.289846", - "version": 0, - "url": "https://ge.xhamster.com/videos/our-girl-friend-caught-us-and-masturbated-looked-on-us-4k-xhTtd47", - "review": 0, - "should_download": 0, - "title": "Unsere Freundin hat uns erwischt und masturbiert und beobachtet uns 4k | xHamster", - "file_name": "Unsere Freundin hat uns erwischt und masturbiert und beobachtet uns 4k [xhTtd47].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/c9ea31dd-5e5f-415d-a014-1b444e3994d2.mp4" - }, - { - "id": "ca5126ef-cf68-4b39-abef-91487f03abef", - "created_date": "2024-08-28 23:21:54.379869", - "last_modified_date": "2024-08-28 23:21:54.379869", - "version": 0, - "url": "https://ge.xhamster.com/videos/nubile-films-best-friends-big-tit-teen-gf-sucks-and-fucks-10941965", - "review": 0, - "should_download": 0, - "title": "Nubile filmt - die beste Freundin des Teenagers mit dicken Titten lutscht und fickt | xHamster", - "file_name": "Nubile filmt - die beste Freundin des Teenagers mit dicken Titten lutscht und fickt [10941965].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/ca5126ef-cf68-4b39-abef-91487f03abef.mp4" - }, - { - "id": "ca719db8-6b3d-41d7-b231-5543f662a197", - "created_date": "2024-09-24 08:11:39.009989", - "last_modified_date": "2024-10-21 16:32:26.280000", - "version": 1, - "url": "https://ge.xhamster.com/videos/private-secretarial-services-1980-7346662", - "review": 0, - "should_download": 0, - "title": "Private Sekret\u00e4rinnendienste - 1980 | xHamster", - "file_name": "Private Sekret\u00e4rinnendienste - 1980 [7346662].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/ca719db8-6b3d-41d7-b231-5543f662a197.mp4" - }, - { - "id": "ca7e19d5-ec77-4648-bb58-d5add967b326", - "created_date": "2024-10-21 15:08:43.550571", - "last_modified_date": "2024-10-21 16:32:29.043000", - "version": 1, - "url": "https://ge.xhamster.com/videos/rita-cardinale-gangbang-and-bukkake-in-the-restaurant-6631530", - "review": 0, - "should_download": 0, - "title": "Rita Cardinale, Gangbang und Bukkake im Restaurant | xHamster", - "file_name": "Rita Cardinale, Gangbang und Bukkake im Restaurant [6631530].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/ca7e19d5-ec77-4648-bb58-d5add967b326.mp4" - }, - { - "id": "ca800ea6-d69a-4ab7-810a-71c50c1bed52", - "created_date": "2024-07-25 07:29:45.977970", - "last_modified_date": "2024-07-25 07:29:45.977970", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepson-and-friend-dp-stepmom-xhGcSVJ", - "review": 0, - "should_download": 0, - "title": "Stiefsohn und Freund doppelpenetrierte Stiefmutter | xHamster", - "file_name": "Stiefsohn und Freund doppelpenetrierte Stiefmutter [xhGcSVJ].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ca800ea6-d69a-4ab7-810a-71c50c1bed52.mp4" - }, - { - "id": "caa4e074-ec16-4ecd-9113-d6bc33f8944f", - "created_date": "2024-11-01 21:08:26.931698", - "last_modified_date": "2024-11-01 21:08:26.931698", - "version": 0, - "url": "https://ge.xhamster.com/videos/classic-1984-caught-from-behind-2-02-xhVXwpE", - "review": 0, - "should_download": 0, - "title": "Klassiker - 1984 - von hinten erwischt 2-02 | xHamster", - "file_name": "Klassiker - 1984 - von hinten erwischt 2-02 [xhVXwpE].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/caa4e074-ec16-4ecd-9113-d6bc33f8944f.mp4" - }, - { - "id": "cab032ae-965c-47a7-866f-1f03c04d9660", - "created_date": "2024-12-29 23:53:27.934520", - "last_modified_date": "2024-12-29 23:53:27.934520", - "version": 0, - "url": "https://ge.xhamster.com/videos/18th-birthday-presents-5864297", - "review": 0, - "should_download": 0, - "title": "18. Geburtstagsgeschenke | xHamster", - "file_name": "18. Geburtstagsgeschenke [5864297].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/cab032ae-965c-47a7-866f-1f03c04d9660.mp4" - }, - { - "id": "cb01b44f-5ed7-418e-887f-22314886c1ef", - "created_date": "2024-07-25 07:29:47.570810", - "last_modified_date": "2025-01-03 11:56:45.657000", - "version": 1, - "url": "https://ge.xhamster.com/videos/palmen-mee-und-nasse-grotten-1979-10328271", - "review": 0, - "should_download": 0, - "title": "Palmen Mee Und Nasse Grotten 1979, Free Porn 1a | xHamster", - "file_name": "Palmen Mee und nasse Grotten (1979) [10328271].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/cb01b44f-5ed7-418e-887f-22314886c1ef.mp4" - }, - { - "id": "cb31e9a8-2496-4062-890f-acca494be1b4", - "created_date": "2024-08-09 21:24:11.520580", - "last_modified_date": "2024-08-16 10:32:05.010000", - "version": 1, - "url": "https://ge.xhamster.com/videos/hot-on-a-stick-1989-xhvvHln", - "review": 0, - "should_download": 0, - "title": "Heiss am Stiel (1989) | xHamster", - "file_name": "Heiss am Stiel (1989) [xhvvHln].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/cb31e9a8-2496-4062-890f-acca494be1b4.mp4" - }, - { - "id": "cb3711f8-7fb7-48e1-8d5c-9c9dd40df0aa", - "created_date": "2024-07-25 07:29:45.974462", - "last_modified_date": "2024-07-25 07:29:45.974462", - "version": 0, - "url": "https://ge.xhamster.com/videos/tushy-babysitter-aspen-ora-fucked-in-the-ass-5481078", - "review": 0, - "should_download": 0, - "title": "Tushy Babysitter Aspen Ora Fucked in the Ass: Free Porn 80 | xHamster", - "file_name": "TUSHY Babysitter Aspen Ora Fucked In The Ass [5481078].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/cb3711f8-7fb7-48e1-8d5c-9c9dd40df0aa.mp4" - }, - { - "id": "cb48a881-8f2d-4fe9-bf20-84958dfc6cb2", - "created_date": "2024-11-01 21:08:26.938165", - "last_modified_date": "2024-11-01 21:08:26.938165", - "version": 0, - "url": "https://ge.xhamster.com/videos/getting-it-from-the-sister-in-law-stripper-xhlJn2d", - "review": 0, - "should_download": 0, - "title": "Es von der stripperin bekommen | xHamster", - "file_name": "Es von der stripperin bekommen [xhlJn2d].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/cb48a881-8f2d-4fe9-bf20-84958dfc6cb2.mp4" - }, - { - "id": "cb48bb60-cac7-4f1d-84ce-90bc0b8716a9", - "created_date": "2024-07-25 07:29:48.125741", - "last_modified_date": "2024-07-25 07:29:48.125741", - "version": 0, - "url": "https://ge.xhamster.com/videos/hardcore-sex-with-stepbrothers-cock-saves-slutty-anna-clair-clouds-distance-relationships-xh5JIId", - "review": 0, - "should_download": 0, - "title": "Hardcore-sex mit dem schwanz des stiefbruers rettet die distanzbeziehungen der versauten Anna Clair Cloud | xHamster", - "file_name": "Hardcore-sex mit dem schwanz des stiefbruers rettet die distanzbeziehungen der versauten Anna Clair Cloud [xh5JIId].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/cb48bb60-cac7-4f1d-84ce-90bc0b8716a9.mp4" - }, - { - "id": "cb6dbae3-4e4c-40ae-846f-42fcd1fb4b24", - "created_date": "2024-07-25 07:29:45.680667", - "last_modified_date": "2024-07-25 07:29:45.680667", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsis-alexa-grace-says-are-you-jerking-off-xhVf6bJ", - "review": 0, - "should_download": 0, - "title": "Stiefschwester Alexa Grace sagt: Wichst du ?! | xHamster", - "file_name": "Stiefschwester Alexa Grace sagt\uff1a Wichst du \uff1f! [xhVf6bJ].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/cb6dbae3-4e4c-40ae-846f-42fcd1fb4b24.mp4" - }, - { - "id": "cb76bb06-afec-4a2d-bdb5-1d6e6cb15a06", - "created_date": "2024-07-25 07:29:44.695820", - "last_modified_date": "2024-07-25 07:29:44.695820", - "version": 0, - "url": "https://ge.xhamster.com/videos/tushy-a-dp-with-my-husband-and-ex-boyfriend-8738285", - "review": 0, - "should_download": 0, - "title": "Tushy, ein Doppelpenetration mit meinem Ehemann und Ex-Freund | xHamster", - "file_name": "Tushy, ein Doppelpenetration mit meinem Ehemann und Ex-Freund [8738285].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/cb76bb06-afec-4a2d-bdb5-1d6e6cb15a06.mp4" - }, - { - "id": "cb96cc1f-f226-4026-a83f-377aea1a3314", - "created_date": "2024-07-25 07:29:45.352004", - "last_modified_date": "2024-07-25 07:29:45.352004", - "version": 0, - "url": "https://ge.xhamster.com/videos/schwester-xhUua2Q", - "review": 0, - "should_download": 0, - "title": "Schwester: Free Hardcore Porn Video ed | xHamster", - "file_name": "Schwester [xhUua2Q].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/cb96cc1f-f226-4026-a83f-377aea1a3314.mp4" - }, - { - "id": "cbc1c104-d114-40e7-8e68-34b27c668ff2", - "created_date": "2024-11-10 16:53:33.473895", - "last_modified_date": "2024-11-10 16:53:33.473895", - "version": 0, - "url": "https://ge.xhamster.com/videos/american-taboo-2-xhhjqYQ", - "review": 0, - "should_download": 0, - "title": "Amerikanisches Tabu 2 | xHamster", - "file_name": "Amerikanisches Tabu 2 [xhhjqYQ].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/cbc1c104-d114-40e7-8e68-34b27c668ff2.mp4" - }, - { - "id": "cce5bd94-7002-4d5f-b7c3-6d5fd92eacde", - "created_date": "2024-12-29 23:53:27.953282", - "last_modified_date": "2024-12-29 23:53:27.953282", - "version": 0, - "url": "https://ge.xhamster.com/videos/step-niece-has-always-been-careful-about-introducing-her-girlfriend-to-her-horny-step-uncle-xhwkAAs", - "review": 0, - "should_download": 0, - "title": "Stiefnichte ist sich immer vorsichtig damit, ihre freundin ihrem geilen stiefon vorgestellt zu haben | xHamster", - "file_name": "Stiefnichte ist sich immer vorsichtig damit, ihre freundin ihrem geilen stiefon vorgestellt zu haben [xhwkAAs].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/cce5bd94-7002-4d5f-b7c3-6d5fd92eacde.mp4" - }, - { - "id": "cd128f71-58a8-4c5e-9600-13c4fcc85456", - "created_date": "2024-10-07 20:47:56.409982", - "last_modified_date": "2024-10-21 16:32:33.427000", - "version": 1, - "url": "https://ge.xhamster.com/videos/double-up-in-daily-work-3877682", - "review": 0, - "should_download": 0, - "title": "Verdoppeln Sie in der t\u00e4glichen Arbeit! | xHamster", - "file_name": "Verdoppeln Sie in der t\u00e4glichen Arbeit! [3877682].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/cd128f71-58a8-4c5e-9600-13c4fcc85456.mp4" - }, - { - "id": "cd288c67-3cf8-4ec5-b87e-0baefe972051", - "created_date": "2024-07-25 07:29:46.929795", - "last_modified_date": "2024-07-25 07:29:46.929795", - "version": 0, - "url": "https://ge.xhamster.com/videos/buddys-secrets-1995-full-movie-xh40CDL", - "review": 0, - "should_download": 0, - "title": "Buddys Geheimnisse (1995) - kompletter Film | xHamster", - "file_name": "Buddys Geheimnisse (1995) - kompletter Film [xh40CDL].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/cd288c67-3cf8-4ec5-b87e-0baefe972051.mp4" - }, - { - "id": "cd449d35-ea0a-4a63-8234-7186f201be82", - "created_date": "2024-07-25 07:29:44.961717", - "last_modified_date": "2024-07-25 07:29:44.961717", - "version": 0, - "url": "https://ge.xhamster.com/videos/mamma-and-daddy-fuck-my-allies-7100646", - "review": 0, - "should_download": 0, - "title": "Mama und Papi ficken meine Verb\u00fcndeten | xHamster", - "file_name": "Mama und Papi ficken meine Verb\u00fcndeten [7100646].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/cd449d35-ea0a-4a63-8234-7186f201be82.mp4" - }, - { - "id": "cd4d35f8-6b74-4802-ab58-c12c7c0f611e", - "created_date": "2024-08-28 23:21:54.377070", - "last_modified_date": "2024-08-28 23:21:54.377070", - "version": 0, - "url": "https://ge.xhamster.com/videos/creepy-older-roommate-turns-on-attention-loving-sexy-thick-redhead-xhZvLzg", - "review": 0, - "should_download": 0, - "title": "Gruseliger \u00e4lterer mitbewohner macht aufmerksamkeit an, liebende sexy dicke rothaarige | xHamster", - "file_name": "Gruseliger \u00e4lterer mitbewohner macht aufmerksamkeit an, liebende sexy dicke rothaarige [xhZvLzg].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/cd4d35f8-6b74-4802-ab58-c12c7c0f611e.mp4" - }, - { - "id": "cdae8c01-14c9-403f-9a23-6ee5468c57fb", - "created_date": "2024-07-25 07:29:44.529507", - "last_modified_date": "2024-07-25 07:29:44.529507", - "version": 0, - "url": "https://ge.xhamster.com/videos/watching-boyfriend-fuck-my-college-best-friend-and-joining-13321068", - "review": 0, - "should_download": 0, - "title": "Zuschauen, wie mein Freund meine beste College-Freundin fickt und mitmachen | xHamster", - "file_name": "Zuschauen, wie mein Freund meine beste College-Freundin fickt und mitmachen [13321068].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/cdae8c01-14c9-403f-9a23-6ee5468c57fb.mp4" - }, - { - "id": "cdf514b1-2f91-4039-82a4-39e8db212a22", - "created_date": "2024-07-25 07:29:46.684217", - "last_modified_date": "2024-07-25 07:29:46.684217", - "version": 0, - "url": "https://ge.xhamster.com/videos/polizei-akademie-full-movie-hd-xhTHgRU", - "review": 0, - "should_download": 0, - "title": "Polizei Akademie Full Movie HD, Free Story HD Porn 2a | xHamster", - "file_name": "Polizei Akademie (Full Movie HD) [xhTHgRU].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/cdf514b1-2f91-4039-82a4-39e8db212a22.mp4" - }, - { - "id": "cebf2bd7-363a-48fe-9fc8-3bbe5907f683", - "created_date": "2024-07-25 07:29:47.786757", - "last_modified_date": "2024-07-25 07:29:47.786757", - "version": 0, - "url": "https://ge.xhamster.com/videos/mom-05-xhnUuKo", - "review": 0, - "should_download": 0, - "title": "Mutter 05 | xHamster", - "file_name": "Mutter 05 [xhnUuKo].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/cebf2bd7-363a-48fe-9fc8-3bbe5907f683.mp4" - }, - { - "id": "cee7b44b-8768-4e67-a25e-2f0587e474de", - "created_date": "2024-12-29 23:53:27.891471", - "last_modified_date": "2024-12-29 23:53:27.891471", - "version": 0, - "url": "https://ge.xhamster.com/videos/ondees-brulantes-1978-brigitte-lahaie-french-vintage-6986484", - "review": 0, - "should_download": 0, - "title": "Ondees Brulantes (1978) - Brigitte Lahaie - franz\u00f6sischer Retro | xHamster", - "file_name": "Ondees Brulantes (1978) - Brigitte Lahaie - franz\u00f6sischer Retro [6986484].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/cee7b44b-8768-4e67-a25e-2f0587e474de.mp4" - }, - { - "id": "cf1f54d3-131c-4be9-8da1-7d836b56f163", - "created_date": "2024-11-10 16:53:33.492237", - "last_modified_date": "2024-11-10 16:53:33.492237", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-whole-family-fucks-at-the-swingers-party-xhzWSEB", - "review": 0, - "should_download": 0, - "title": "Die ganze familie fickt auf der Swingerparty | xHamster", - "file_name": "Die ganze familie fickt auf der Swingerparty [xhzWSEB].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/cf1f54d3-131c-4be9-8da1-7d836b56f163.mp4" - }, - { - "id": "cf758d88-0b92-4dd4-8dd8-4ca611f0cff2", - "created_date": "2024-07-25 07:29:45.770613", - "last_modified_date": "2024-07-25 07:29:45.770613", - "version": 0, - "url": "https://ge.xhamster.com/videos/first-time-exhibition-xhGRpbG", - "review": 0, - "should_download": 0, - "title": "Zum ersten mal ausstellung | xHamster", - "file_name": "Zum ersten mal ausstellung [xhGRpbG].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/cf758d88-0b92-4dd4-8dd8-4ca611f0cff2.mp4" - }, - { - "id": "cf9fa6bd-e2e2-4250-8def-af2a70faaf4b", - "created_date": "2024-07-25 07:29:45.692438", - "last_modified_date": "2024-07-25 07:29:45.692438", - "version": 0, - "url": "https://ge.xhamster.com/videos/athena-anderson-says-i-love-watching-you-fuck-your-stepmom-s2-e3-xhokOrB", - "review": 0, - "should_download": 0, - "title": "Athena Anderson sagt: \"Ich liebe es, dich beim Ficken mit deiner Stiefmutter zu beobachten\" s2: e3 | xHamster", - "file_name": "Athena Anderson sagt\uff1a \uff02Ich liebe es, dich beim Ficken mit deiner Stiefmutter zu beobachten\uff02 s2\uff1a e3 [xhokOrB].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/cf9fa6bd-e2e2-4250-8def-af2a70faaf4b.mp4" - }, - { - "id": "cfaa9782-95c1-4551-b1d1-dc6d8c43f717", - "created_date": "2024-07-25 07:29:44.492215", - "last_modified_date": "2024-07-25 07:29:44.492215", - "version": 0, - "url": "https://ge.xhamster.com/videos/sabine-mallory-secretary-14136574", - "review": 0, - "should_download": 0, - "title": "Sabine Mallory - Sekret\u00e4rin | xHamster", - "file_name": "Sabine Mallory - Sekret\u00e4rin [14136574].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/cfaa9782-95c1-4551-b1d1-dc6d8c43f717.mp4" - }, - { - "id": "cfb65461-ef2d-4c5b-9fd4-aa3d6f12693c", - "created_date": "2024-07-25 07:29:45.043862", - "last_modified_date": "2024-07-25 07:29:45.043862", - "version": 0, - "url": "https://ge.xhamster.com/videos/slutty-stepmom-makes-a-deal-with-step-daughters-cute-tutor-to-date-and-fuck-her-virgin-girl-xhEpxEg", - "review": 0, - "should_download": 0, - "title": "Versaute stiefmutter macht einen deal mit dem s\u00fc\u00dfen lehrer der stieftochter und fickt ihr jungfr\u00e4uliches m\u00e4dchen | xHamster", - "file_name": "Versaute stiefmutter macht einen deal mit dem s\u00fc\u00dfen lehrer der stieftochter und fickt ihr jungfr\u00e4uliches m\u00e4dchen [xhEpxEg].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/cfb65461-ef2d-4c5b-9fd4-aa3d6f12693c.mp4" - }, - { - "id": "cfb9b3e7-73bc-4733-9622-a26c6e277d30", - "created_date": "2024-07-25 07:29:46.081091", - "last_modified_date": "2024-07-25 07:29:46.081091", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-kinky-family-8655519", - "review": 0, - "should_download": 0, - "title": "Die versaute Familie | xHamster", - "file_name": "Die versaute Familie [8655519].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/cfb9b3e7-73bc-4733-9622-a26c6e277d30.mp4" - }, - { - "id": "cfe14607-e107-47dc-9bf4-a3c6db42255a", - "created_date": "2024-07-25 07:29:48.110870", - "last_modified_date": "2024-07-25 07:29:48.110870", - "version": 0, - "url": "https://ge.xhamster.com/videos/freeuse-house-stepgrandpa-bangs-his-step-daughter-and-step-granddaughter-whenever-he-feels-like-it-xhcyjxM", - "review": 0, - "should_download": 0, - "title": "Freeuse House - Stepgrandpa Bangs His Step-daughter and Step-granddaughter Whenever He Feels Like it | xHamster", - "file_name": "FreeUse House - Stepgrandpa Bangs His Step-Daughter And Step-granddaughter Whenever He Feels Like It [xhcyjxM].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/cfe14607-e107-47dc-9bf4-a3c6db42255a.mp4" - }, - { - "id": "d0047e18-7a02-4229-bffb-418f95c8249e", - "created_date": "2024-07-25 07:29:44.914322", - "last_modified_date": "2024-07-25 07:29:44.914322", - "version": 0, - "url": "https://ge.xhamster.com/videos/great-exciting-threesome-on-the-boat-xhdWomb", - "review": 0, - "should_download": 0, - "title": "Toller aufregender Dreier auf dem Boot! | xHamster", - "file_name": "Toller aufregender Dreier auf dem Boot! [xhdWomb].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d0047e18-7a02-4229-bffb-418f95c8249e.mp4" - }, - { - "id": "d019d154-1aec-4fa9-9f0b-da29555a2918", - "created_date": "2024-07-25 07:29:47.514381", - "last_modified_date": "2024-07-25 07:29:47.514381", - "version": 0, - "url": "https://ge.xhamster.com/videos/teeny-strich-privat-10313120", - "review": 0, - "should_download": 0, - "title": "Teeny strich privat | xHamster", - "file_name": "Teeny strich privat [10313120].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d019d154-1aec-4fa9-9f0b-da29555a2918.mp4" - }, - { - "id": "d06a8498-c83f-43fe-acac-5155f1b88d15", - "created_date": "2024-07-25 07:29:45.408111", - "last_modified_date": "2024-07-25 07:29:45.408111", - "version": 0, - "url": "https://ge.xhamster.com/videos/mein-verfickter-urlaub-episode-5-xh2vztC", - "review": 0, - "should_download": 0, - "title": "Mein Verfickter Urlaub - Episode 5, Free Porn 73 | xHamster", - "file_name": "Mein verfickter Urlaub - Episode 5 [xh2vztC].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d06a8498-c83f-43fe-acac-5155f1b88d15.mp4" - }, - { - "id": "d0ccd522-6adb-4bd1-9c5f-fda0a57469b1", - "created_date": "2024-12-30 18:49:09.989000", - "last_modified_date": "2025-01-03 01:46:20.592000", - "version": 2, - "url": "https://ge.xhamster.com/videos/step-mom-caught-her-daughter-with-a-classmate-and-join-to-them-13978461", - "review": 0, - "should_download": 0, - "title": "Stiefmutter erwischt ihre Tochter mit einem Mitsch\u00fcler und schlie\u00dft sich ihnen an | xHamster", - "file_name": "Stiefmutter erwischt ihre Tochter mit einem Mitsch\u00fcler und schlie\u00dft sich ihnen an [13978461].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/d0ccd522-6adb-4bd1-9c5f-fda0a57469b1.mp4" - }, - { - "id": "d0d4f87c-e479-4823-ba30-a37a95ae9fd0", - "created_date": "2024-07-25 07:29:44.864929", - "last_modified_date": "2024-07-25 07:29:44.864929", - "version": 0, - "url": "https://ge.xhamster.com/videos/perfect-assed-wet-teens-win-a-swimming-competition-xh2A5G2", - "review": 0, - "should_download": 0, - "title": "Perfekte feuchte Teenager gewinnen einen Schwimmwettbewerb | xHamster", - "file_name": "Perfekte feuchte Teenager gewinnen einen Schwimmwettbewerb [xh2A5G2].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/d0d4f87c-e479-4823-ba30-a37a95ae9fd0.mp4" - }, - { - "id": "d0e9ba27-5cc6-4521-b22f-919c45ad0e8b", - "created_date": "2024-07-25 07:29:48.052248", - "last_modified_date": "2024-07-25 07:29:48.052248", - "version": 0, - "url": "https://ge.xhamster.com/videos/private-private-com-big-boobs-orgy-in-the-country-9737301", - "review": 0, - "should_download": 0, - "title": "Private.com Private.com Orgie mit gro\u00dfen M\u00f6psen auf dem Land | xHamster", - "file_name": "Private.com Private.com Orgie mit gro\u00dfen M\u00f6psen auf dem Land [9737301].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d0e9ba27-5cc6-4521-b22f-919c45ad0e8b.mp4" - }, - { - "id": "d11741e4-5e94-4595-b161-57bb7cf0df85", - "created_date": "2024-07-25 07:29:47.820510", - "last_modified_date": "2024-07-25 07:29:47.820510", - "version": 0, - "url": "https://ge.xhamster.com/videos/immer-muss-ich-mit-meinem-stiefbruder-schlafen-xhSzOSx", - "review": 0, - "should_download": 0, - "title": "Immer Muss Ich Mit Meinem Stiefbruder Schlafen: HD Porn b1 | xHamster", - "file_name": "Immer muss ich mit meinem Stiefbruder schlafen [xhSzOSx].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d11741e4-5e94-4595-b161-57bb7cf0df85.mp4" - }, - { - "id": "d13bc711-a755-4a68-817a-92b2ddd6bc58", - "created_date": "2024-11-01 21:08:26.936468", - "last_modified_date": "2024-11-01 21:08:26.936468", - "version": 0, - "url": "https://ge.xhamster.com/videos/die-geile-heidi-1990264", - "review": 0, - "should_download": 0, - "title": "Die Geile Heidi: Free Hardcore Porn Video ce | xHamster", - "file_name": "Die Geile Heidi [1990264].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/d13bc711-a755-4a68-817a-92b2ddd6bc58.mp4" - }, - { - "id": "d1aaf4bc-b307-45f9-94df-620642580388", - "created_date": "2024-07-25 07:29:47.766365", - "last_modified_date": "2024-07-25 07:29:47.766365", - "version": 0, - "url": "https://ge.xhamster.com/videos/college-wild-party-1424365", - "review": 0, - "should_download": 0, - "title": "College-wilde Party | xHamster", - "file_name": "College-wilde Party [1424365].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d1aaf4bc-b307-45f9-94df-620642580388.mp4" - }, - { - "id": "d1ff3e98-3148-43b0-bc48-3b332a0bb0ff", - "created_date": "2024-07-25 07:29:47.296158", - "last_modified_date": "2024-07-25 07:29:47.296158", - "version": 0, - "url": "https://ge.xhamster.com/videos/anal-nuru-massage-with-chanel-preston-7334836", - "review": 0, - "should_download": 0, - "title": "Anal-Nuru-Massage mit Chanel Preston | xHamster", - "file_name": "Anal-Nuru-Massage mit Chanel Preston [7334836].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d1ff3e98-3148-43b0-bc48-3b332a0bb0ff.mp4" - }, - { - "id": "d26f3ac9-1c23-4a06-8c68-ee8d9e27c6f2", - "created_date": "2024-07-25 07:29:45.654680", - "last_modified_date": "2024-07-25 07:29:45.654680", - "version": 0, - "url": "https://ge.xhamster.com/videos/neighborhood-whore-the-drive-in-xhlnc7o", - "review": 0, - "should_download": 0, - "title": "Nachbarschaftshure bei der Einfahrt | xHamster", - "file_name": "Nachbarschaftshure bei der Einfahrt [xhlnc7o].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d26f3ac9-1c23-4a06-8c68-ee8d9e27c6f2.mp4" - }, - { - "id": "d26f8f8a-a819-4f35-94fe-115a2434729e", - "created_date": "2024-07-25 07:29:47.993485", - "last_modified_date": "2024-07-25 07:29:47.993485", - "version": 0, - "url": "https://ge.xhamster.com/videos/barely-legal-5-6-12629903", - "review": 0, - "should_download": 0, - "title": "Kaum legal 5 & 6 | xHamster", - "file_name": "Kaum legal 5 & 6 [12629903].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d26f8f8a-a819-4f35-94fe-115a2434729e.mp4" - }, - { - "id": "d297d126-3dbf-495a-8b4f-26e4b9b8117a", - "created_date": "2024-07-25 07:29:44.405386", - "last_modified_date": "2024-07-25 07:29:44.405386", - "version": 0, - "url": "https://ge.xhamster.com/videos/mommy4k-mommy-does-it-again-xhDFhEs", - "review": 0, - "should_download": 0, - "title": "MOMMY4K. Mama macht es wieder | xHamster", - "file_name": "MOMMY4K. Mama macht es wieder [xhDFhEs].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/d297d126-3dbf-495a-8b4f-26e4b9b8117a.mp4" - }, - { - "id": "d2f21e84-901a-4451-ba9a-089382dd5be9", - "created_date": "2024-07-25 07:29:44.262139", - "last_modified_date": "2024-07-25 07:29:44.262139", - "version": 0, - "url": "https://ge.xhamster.com/videos/taboo-family-the-last-taboo-xhHFfec", - "review": 0, - "should_download": 0, - "title": "Tabu-Familie - das letzte Tabu | xHamster", - "file_name": "Tabu-Familie - das letzte Tabu [xhHFfec].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d2f21e84-901a-4451-ba9a-089382dd5be9.mp4" - }, - { - "id": "d353d233-312f-4048-b7ab-de91de18eb28", - "created_date": "2024-07-25 07:29:45.207752", - "last_modified_date": "2024-07-25 07:29:45.207752", - "version": 0, - "url": "https://ge.xhamster.com/videos/tushy-first-double-penetration-for-redhead-kimberly-brix-5985685", - "review": 0, - "should_download": 0, - "title": "Tushy, erste Doppelpenetration f\u00fcr rothaarige Kimberly Brix | xHamster", - "file_name": "Tushy, erste Doppelpenetration f\u00fcr rothaarige Kimberly Brix [5985685].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d353d233-312f-4048-b7ab-de91de18eb28.mp4" - }, - { - "id": "d368b14b-c61b-4d5e-ae99-557e91749dc7", - "created_date": "2024-07-25 07:29:45.123782", - "last_modified_date": "2024-07-25 07:29:45.123782", - "version": 0, - "url": "https://ge.xhamster.com/videos/game-orgy-xhJtTBz", - "review": 0, - "should_download": 0, - "title": "Spielorgie | xHamster", - "file_name": "Spielorgie [xhJtTBz].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d368b14b-c61b-4d5e-ae99-557e91749dc7.mp4" - }, - { - "id": "d369facc-3ceb-4d1a-adf4-a91bc8e23c52", - "created_date": "2024-07-25 07:29:44.503415", - "last_modified_date": "2024-07-25 07:29:44.503415", - "version": 0, - "url": "https://ge.xhamster.com/videos/two-couples-having-sex-on-a-boat-xhjktWw", - "review": 0, - "should_download": 0, - "title": "Zwei Paare haben Sex auf einem Boot | xHamster", - "file_name": "Zwei Paare haben Sex auf einem Boot [xhjktWw].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d369facc-3ceb-4d1a-adf4-a91bc8e23c52.mp4" - }, - { - "id": "d3a959b2-4c65-43bc-a002-2c3c6bc342ec", - "created_date": "2024-07-25 07:29:45.586939", - "last_modified_date": "2024-07-25 07:29:45.586939", - "version": 0, - "url": "https://ge.xhamster.com/videos/too-much-too-soon-13654213", - "review": 0, - "should_download": 0, - "title": "Zu viel zu fr\u00fch | xHamster", - "file_name": "Zu viel zu fr\u00fch [13654213].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d3a959b2-4c65-43bc-a002-2c3c6bc342ec.mp4" - }, - { - "id": "d3b6a04f-6f86-4861-a198-476b35a9292d", - "created_date": "2024-07-25 07:29:45.154438", - "last_modified_date": "2024-07-25 07:29:45.154438", - "version": 0, - "url": "https://ge.xhamster.com/videos/time-smut-pt-3-a-day-at-the-beach-in-a-freeuse-world-xh5w7CI", - "review": 0, - "should_download": 0, - "title": "Time Smut Teil 3 - ein Tag am Strand in einer freien Welt. | xHamster", - "file_name": "Time Smut Teil 3 - ein Tag am Strand in einer freien Welt. [xh5w7CI].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d3b6a04f-6f86-4861-a198-476b35a9292d.mp4" - }, - { - "id": "d44a9906-26ed-4a9d-a032-38472e45c354", - "created_date": "2024-07-25 07:29:44.487806", - "last_modified_date": "2024-07-25 07:29:44.487806", - "version": 0, - "url": "https://ge.xhamster.com/videos/blonde-double-teamed-by-the-pool-side-xhXKOfx", - "review": 0, - "should_download": 0, - "title": "Blondine doppelt am pool genommen | xHamster", - "file_name": "Blondine doppelt am pool genommen [xhXKOfx].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d44a9906-26ed-4a9d-a032-38472e45c354.mp4" - }, - { - "id": "d4e74556-206c-4f6b-a13c-6f63442a604f", - "created_date": "2024-07-25 07:29:45.745758", - "last_modified_date": "2024-07-25 07:29:45.745758", - "version": 0, - "url": "https://ge.xhamster.com/videos/little-sisters-1972-remastered-7546134", - "review": 0, - "should_download": 0, - "title": "Little Sisters - 1972 (remastered) | xHamster", - "file_name": "Little Sisters - 1972 (remastered) [7546134].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/d4e74556-206c-4f6b-a13c-6f63442a604f.mp4" - }, - { - "id": "d4f43d34-3f3e-46f5-90fb-ab87ead26c7f", - "created_date": "2024-07-25 07:29:47.344326", - "last_modified_date": "2024-07-25 07:29:47.344326", - "version": 0, - "url": "https://ge.xhamster.com/videos/freeuse-stepsisters-are-the-best-ava-sinclaire-and-aften-opal-free-use-fantasy-xhfceZX", - "review": 0, - "should_download": 0, - "title": "Freeuse - Stiefschwestern sind die besten - Ava Sinclaire und oft opalfreie Fantasie | xHamster", - "file_name": "Freeuse - Stiefschwestern sind die besten - Ava Sinclaire und oft opalfreie Fantasie [xhfceZX].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d4f43d34-3f3e-46f5-90fb-ab87ead26c7f.mp4" - }, - { - "id": "d5361e26-3ef5-4254-9748-73f45803856d", - "created_date": "2024-07-25 07:29:46.946349", - "last_modified_date": "2024-07-25 07:29:46.946349", - "version": 0, - "url": "https://ge.xhamster.com/videos/a-highly-dysfunctional-family-xhYDFH4", - "review": 0, - "should_download": 0, - "title": "Eine Familie mit hoher Dysfunktion | xHamster", - "file_name": "Eine Familie mit hoher Dysfunktion [xhYDFH4].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d5361e26-3ef5-4254-9748-73f45803856d.mp4" - }, - { - "id": "d5550ccd-f756-43a3-8db5-53a47a50e5bf", - "created_date": "2024-07-25 07:29:47.741744", - "last_modified_date": "2024-07-25 07:29:47.741744", - "version": 0, - "url": "https://ge.xhamster.com/videos/busty-redhead-wife-syren-de-mer-gets-pounded-by-her-cuckold-husbands-boss-in-front-of-him-mylf-xhzOBv3", - "review": 0, - "should_download": 0, - "title": "Die vollbusige rothaarige ehefrau syren de mer wird vom chef ihres cuckold-ehemanns vor ihm - mylf - geritten | xHamster", - "file_name": "Die vollbusige rothaarige ehefrau syren de mer wird vom chef ihres cuckold-ehemanns vor ihm - mylf - geritten [xhzOBv3].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d5550ccd-f756-43a3-8db5-53a47a50e5bf.mp4" - }, - { - "id": "d5c19748-f124-4d2c-8ca4-b45e471570b5", - "created_date": "2024-07-25 07:29:47.016195", - "last_modified_date": "2024-07-25 07:29:47.016195", - "version": 0, - "url": "https://ge.xhamster.com/videos/hot-italian-mom-fucks-step-sons-best-friend-artemisia-love-xhEcGzf", - "review": 0, - "should_download": 0, - "title": "Hei\u00dfe italienische Mutter fickt den besten Freund des Sohnes - Artemisia Love | xHamster", - "file_name": "Hei\u00dfe italienische Mutter fickt den besten Freund des Sohnes - Artemisia Love [xhEcGzf].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d5c19748-f124-4d2c-8ca4-b45e471570b5.mp4" - }, - { - "id": "d60f7bdd-7fa2-4f77-b5e2-0b7b06e7d5a0", - "created_date": "2024-07-25 07:29:45.270905", - "last_modified_date": "2024-07-25 07:29:45.270905", - "version": 0, - "url": "https://ge.xhamster.com/videos/eighteen-lust-2005-full-movie-10776579", - "review": 0, - "should_download": 0, - "title": "Eighteen Lust (2005) kompletter Film | xHamster", - "file_name": "Eighteen Lust (2005) kompletter Film [10776579].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/d60f7bdd-7fa2-4f77-b5e2-0b7b06e7d5a0.mp4" - }, - { - "id": "d63f6456-3617-4c71-807e-3f75646746de", - "created_date": "2024-12-29 23:53:27.954241", - "last_modified_date": "2024-12-29 23:53:27.954241", - "version": 0, - "url": "https://ge.xhamster.com/videos/insatiable-1980-xhz6KxN", - "review": 0, - "should_download": 0, - "title": "Uners\u00e4ttlich (1980) | xHamster", - "file_name": "Uners\u00e4ttlich (1980) [xhz6KxN].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/d63f6456-3617-4c71-807e-3f75646746de.mp4" - }, - { - "id": "d6888f01-a201-43a0-a45f-85c91174378e", - "created_date": "2024-07-25 07:29:45.846800", - "last_modified_date": "2024-07-25 07:29:45.846800", - "version": 0, - "url": "https://ge.xhamster.com/videos/ok-if-you-lick-something-that-does-not-mean-that-you-own-it-milf-summer-hart-shouts-xhXbfWu", - "review": 0, - "should_download": 0, - "title": "\"Ok, wenn du etwas leckst, hei\u00dft das nicht, dass du es besitzt!\" MILF Summer Hart schreit | xHamster", - "file_name": "\uff02Ok, wenn du etwas leckst, hei\u00dft das nicht, dass du es besitzt!\uff02 MILF Summer Hart schreit [xhXbfWu].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d6888f01-a201-43a0-a45f-85c91174378e.mp4" - }, - { - "id": "d690a959-d2c0-494f-b095-bdf290ed9772", - "created_date": "2024-07-25 07:29:46.565242", - "last_modified_date": "2024-07-25 07:29:46.565242", - "version": 0, - "url": "https://ge.xhamster.com/videos/german-farm-1220162", - "review": 0, - "should_download": 0, - "title": "Deutscher Bauernhof | xHamster", - "file_name": "Deutscher Bauernhof [1220162].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d690a959-d2c0-494f-b095-bdf290ed9772.mp4" - }, - { - "id": "d699d176-3ec4-4258-a5f9-cc1e13c46dd1", - "created_date": "2024-07-25 07:29:44.293115", - "last_modified_date": "2024-07-25 07:29:44.293115", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsis-says-i-didnt-know-stepbro-had-such-a-big-fucking-dick-xhOrCS8", - "review": 0, - "should_download": 0, - "title": "Stiefschwester sagt, ich wusste nicht, dass Stiefbruder einen so gro\u00dfen verdammten Schwanz hatte | xHamster", - "file_name": "Stiefschwester sagt, ich wusste nicht, dass Stiefbruder einen so gro\u00dfen verdammten Schwanz hatte [xhOrCS8].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d699d176-3ec4-4258-a5f9-cc1e13c46dd1.mp4" - }, - { - "id": "d6e77df1-2085-483e-b25f-fcea57e7a2ea", - "created_date": "2024-07-25 07:29:47.292495", - "last_modified_date": "2024-07-25 07:29:47.292495", - "version": 0, - "url": "https://ge.xhamster.com/videos/die-total-versaute-ballettschule-full-german-movie-xhRPuHB", - "review": 0, - "should_download": 0, - "title": "Die Total Versaute Ballettschule- Full German Movie | xHamster", - "file_name": "Die Total Versaute Ballettschule- full german movie [xhRPuHB].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d6e77df1-2085-483e-b25f-fcea57e7a2ea.mp4" - }, - { - "id": "d747134e-3996-4933-aed0-9a610e4d1407", - "created_date": "2024-07-25 07:29:45.789670", - "last_modified_date": "2024-07-25 07:29:45.789670", - "version": 0, - "url": "https://ge.xhamster.com/videos/dinos-first-class-03-road-runner-xhWyDjg", - "review": 0, - "should_download": 0, - "title": "Dinos erste Klasse 03: Stra\u00dfenl\u00e4ufer | xHamster", - "file_name": "Dinos erste Klasse 03\uff1a Stra\u00dfenl\u00e4ufer [xhWyDjg].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d747134e-3996-4933-aed0-9a610e4d1407.mp4" - }, - { - "id": "d769424d-4c99-4c71-927c-a5b2e74de47b", - "created_date": "2024-07-25 07:29:46.584008", - "last_modified_date": "2024-07-25 07:29:46.584008", - "version": 0, - "url": "https://ge.xhamster.com/videos/secrets-1984-xhO45a8", - "review": 0, - "should_download": 0, - "title": "Suss und schamlos - Secrets (1984) | xHamster", - "file_name": "Suss und schamlos - Secrets (1984) [xhO45a8].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d769424d-4c99-4c71-927c-a5b2e74de47b.mp4" - }, - { - "id": "d77a52f6-f233-4d0e-ac03-0ad11f2b15d6", - "created_date": "2024-07-25 07:29:45.782227", - "last_modified_date": "2024-07-25 07:29:45.782227", - "version": 0, - "url": "https://ge.xhamster.com/videos/teeny-gangbang-party-hier-darf-jeder-mal-xhory9I", - "review": 0, - "should_download": 0, - "title": "Teeny Gangbang Party Hier Darf Jeder Mal, Porn 17 | xHamster", - "file_name": "Teeny Gangbang Party hier darf jeder mal [xhory9I].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d77a52f6-f233-4d0e-ac03-0ad11f2b15d6.mp4" - }, - { - "id": "d79d6de4-017e-4e6c-9a9b-e8634163b076", - "created_date": "2024-07-25 07:29:45.851319", - "last_modified_date": "2025-01-03 11:56:48.588000", - "version": 1, - "url": "https://ge.xhamster.com/videos/top-rated-classic-22-4k-restoration-11447106", - "review": 0, - "should_download": 0, - "title": "Bestbewertete klassische 22 - 4k-Restauration | xHamster", - "file_name": "Bestbewertete klassische 22 - 4k-Restauration [11447106].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d79d6de4-017e-4e6c-9a9b-e8634163b076.mp4" - }, - { - "id": "d7b3598d-d684-4aff-94d8-603cf0d2055c", - "created_date": "2024-07-25 07:29:45.476321", - "last_modified_date": "2024-07-25 07:29:45.476321", - "version": 0, - "url": "https://ge.xhamster.com/videos/sluts-getting-facial-after-sucking-dick-and-taking-cocks-in-fuck-holes-in-bus-xhUqETE", - "review": 0, - "should_download": 0, - "title": "Schlampen bekommen Gesichtsbesamung, nachdem sie Schwanz gelutscht haben und Schw\u00e4nze in Fickl\u00f6cher im Bus genommen haben | xHamster", - "file_name": "Schlampen bekommen Gesichtsbesamung, nachdem sie Schwanz gelutscht haben und Schw\u00e4nze in Fickl\u00f6cher im Bus genommen haben [xhUqETE].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/d7b3598d-d684-4aff-94d8-603cf0d2055c.mp4" - }, - { - "id": "d7c54e9b-feba-4ec3-a5fa-fbafba81519b", - "created_date": "2024-09-24 08:11:38.996727", - "last_modified_date": "2024-10-21 16:32:39.982000", - "version": 1, - "url": "https://ge.xhamster.com/videos/stepmom-lets-stepdaughter-jointhreesome-with-shockingly-huge-cock-xhxuLhW", - "review": 0, - "should_download": 0, - "title": "Stiefmutter l\u00e4sst stieftochter zu dreier mit schockierend riesigem schwanz kommen | xHamster", - "file_name": "Stiefmutter l\u00e4sst stieftochter zu dreier mit schockierend riesigem schwanz kommen [xhxuLhW].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/d7c54e9b-feba-4ec3-a5fa-fbafba81519b.mp4" - }, - { - "id": "d7dbb315-ab66-4611-a0bd-59cf032b6a57", - "created_date": "2024-09-24 08:11:38.998949", - "last_modified_date": "2024-10-21 16:32:46.162000", - "version": 1, - "url": "https://ge.xhamster.com/videos/i-want-you-and-your-friend-to-fuck-me-xhD6tWB", - "review": 0, - "should_download": 0, - "title": "Ich will, dass du und dein freund mich ficken! | xHamster", - "file_name": "Ich will, dass du und dein freund mich ficken! [xhD6tWB].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/d7dbb315-ab66-4611-a0bd-59cf032b6a57.mp4" - }, - { - "id": "d80e135f-12ff-41b3-b256-80a4469ce197", - "created_date": "2024-08-08 00:54:26.821675", - "last_modified_date": "2024-08-16 11:04:35.691000", - "version": 1, - "url": "https://ge.xhamster.com/videos/simon-says-take-off-your-pants-liz-jordan-tells-lacy-lennon-and-stepbro-s17-e6-xhPZhDs", - "review": 0, - "should_download": 0, - "title": "\"Simon sagt, zieh deine hose aus\", Liz Jordan sagt Lacy Lennon und stiefbruer - S17: e6 | xHamster", - "file_name": "\uff02Simon sagt, zieh deine hose aus\uff02, Liz Jordan sagt Lacy Lennon und stiefbruer - S17\uff1a e6 [xhPZhDs].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d80e135f-12ff-41b3-b256-80a4469ce197.mp4" - }, - { - "id": "d841d21e-562f-4384-9dc0-ee8112bbd3a9", - "created_date": "2024-07-25 07:29:47.627749", - "last_modified_date": "2024-07-25 07:29:47.627749", - "version": 0, - "url": "https://ge.xhamster.com/videos/kuken-3-full-movie-xhV9Ozo", - "review": 0, - "should_download": 0, - "title": "Kuken 3 Full Movie: Free Big Cock Porn Video f2 | xHamster", - "file_name": "KUKEN 3 (Full Movie) [xhV9Ozo].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d841d21e-562f-4384-9dc0-ee8112bbd3a9.mp4" - }, - { - "id": "d876509b-036b-48fe-ad31-6f011500ffbf", - "created_date": "2024-11-01 21:08:26.934780", - "last_modified_date": "2024-11-01 21:08:26.934780", - "version": 0, - "url": "https://ge.xhamster.com/videos/in-der-scheune-die-notgeile-blonde-gefickt-xhNdWNr", - "review": 0, - "should_download": 0, - "title": "In Der Scheune Die Notgeile Blonde Gefickt: Free HD Porn c4 | xHamster", - "file_name": "In der Scheune die Notgeile Blonde gefickt [xhNdWNr].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/d876509b-036b-48fe-ad31-6f011500ffbf.mp4" - }, - { - "id": "d9591140-4e61-478e-8185-064aef0a9c32", - "created_date": "2024-11-10 16:53:33.469846", - "last_modified_date": "2024-11-10 16:53:33.469846", - "version": 0, - "url": "https://ge.xhamster.com/videos/humiliation-01-4539633", - "review": 0, - "should_download": 0, - "title": "Dem\u00fctigung 01 | xHamster", - "file_name": "Dem\u00fctigung 01 [4539633].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/d9591140-4e61-478e-8185-064aef0a9c32.mp4" - }, - { - "id": "d98f3cd5-7dad-4e28-bc09-83947c3707c9", - "created_date": "2024-10-14 20:33:38.256401", - "last_modified_date": "2024-10-21 16:32:53.696000", - "version": 1, - "url": "https://ge.xhamster.com/videos/la-grande-enfilade-1978-8405888", - "review": 0, - "should_download": 0, - "title": "La Grande Enfilade - 1978, Free Vintage Porn 89 | xHamster", - "file_name": "La Grande Enfilade - 1978 [8405888].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/d98f3cd5-7dad-4e28-bc09-83947c3707c9.mp4" - }, - { - "id": "d9f1d5ad-e13e-4c37-9b1d-c4e4f7fd2797", - "created_date": "2024-07-25 07:29:46.666589", - "last_modified_date": "2024-07-25 07:29:46.666589", - "version": 0, - "url": "https://ge.xhamster.com/videos/anja-und-der-herbergsvater-germany-1998-4518894", - "review": 0, - "should_download": 0, - "title": "Anja Und Der Herbergsvater Germany 1998, Porn 35 | xHamster", - "file_name": "Anja und der Herbergsvater (Germany 1998) [4518894].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/d9f1d5ad-e13e-4c37-9b1d-c4e4f7fd2797.mp4" - }, - { - "id": "da16d6a4-08e7-4ed2-96ad-1b91182b302c", - "created_date": "2024-07-25 07:29:45.583419", - "last_modified_date": "2024-07-25 07:29:45.583419", - "version": 0, - "url": "https://ge.xhamster.com/videos/young-slut-getting-pounded-outdoors-xhaE8my", - "review": 0, - "should_download": 0, - "title": "Junge schlampe wird im freien geritten | xHamster", - "file_name": "Junge schlampe wird im freien geritten [xhaE8my].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/da16d6a4-08e7-4ed2-96ad-1b91182b302c.mp4" - }, - { - "id": "da4cc5be-ce66-4ee9-9328-132cf7cdfce8", - "created_date": "2024-07-25 07:29:45.033009", - "last_modified_date": "2024-07-25 07:29:45.033009", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepmom-says-you-should-really-put-those-tits-away-xhGY2TV", - "review": 0, - "should_download": 0, - "title": "Stiefmutter sagt, du solltest diese Titten wirklich weglegen! | xHamster", - "file_name": "Stiefmutter sagt, du solltest diese Titten wirklich weglegen! [xhGY2TV].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/da4cc5be-ce66-4ee9-9328-132cf7cdfce8.mp4" - }, - { - "id": "da5bbdb5-9746-43ba-83bc-ebc61502f383", - "created_date": "2024-07-25 07:29:47.949494", - "last_modified_date": "2024-07-25 07:29:47.949494", - "version": 0, - "url": "https://ge.xhamster.com/videos/she-loves-making-peepers-horny-they-may-cum-on-her-mouth-xhEfVST", - "review": 0, - "should_download": 0, - "title": "Sie liebt es, Spanner geil zu machen! sie k\u00f6nnen auf ihren Mund kommen | xHamster", - "file_name": "Sie liebt es, Spanner geil zu machen! sie k\u00f6nnen auf ihren Mund kommen [xhEfVST].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/da5bbdb5-9746-43ba-83bc-ebc61502f383.mp4" - }, - { - "id": "da97c0e7-9a66-4ef4-9721-99f87a73a1de", - "created_date": "2024-07-25 07:29:45.178074", - "last_modified_date": "2024-07-25 07:29:45.178074", - "version": 0, - "url": "https://ge.xhamster.com/videos/horny-lexi-luna-cant-resist-her-boyfriend-college-sons-hard-rock-dick-brazzers-xhWqdyJ", - "review": 0, - "should_download": 0, - "title": "Geile lexi luna kann dem harten steinschwanz - brazzers des college-sohns ihres freundes nicht widerstehen | xHamster", - "file_name": "Geile lexi luna kann dem harten steinschwanz - brazzers des college-sohns ihres freundes nicht widerstehen [xhWqdyJ].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/da97c0e7-9a66-4ef4-9721-99f87a73a1de.mp4" - }, - { - "id": "daa6db62-8803-4ef8-894b-4b6a8faa7cc4", - "created_date": "2024-07-25 07:29:47.953245", - "last_modified_date": "2024-07-25 07:29:47.953245", - "version": 0, - "url": "https://ge.xhamster.desi/videos/i-was-surprised-by-my-reaction-to-my-naked-stepsister-xhdXhea", - "review": 0, - "should_download": 0, - "title": "Ich war von meiner Reaktion auf meine nackte Stiefschwester \u00fcberrascht. | xHamster", - "file_name": "Ich war von meiner Reaktion auf meine nackte Stiefschwester \u00fcberrascht. [xhdXhea].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/daa6db62-8803-4ef8-894b-4b6a8faa7cc4.mp4" - }, - { - "id": "dab2b160-3b18-4206-8ea6-9adce974b705", - "created_date": "2024-07-25 07:29:46.144530", - "last_modified_date": "2024-07-25 07:29:46.144530", - "version": 0, - "url": "https://ge.xhamster.com/videos/horny-weekend-1986-10623425", - "review": 0, - "should_download": 0, - "title": "Geiles Wochenende (1986) | xHamster", - "file_name": "Geiles Wochenende (1986) [10623425].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/dab2b160-3b18-4206-8ea6-9adce974b705.mp4" - }, - { - "id": "daf8430b-71c4-4658-a070-018178829033", - "created_date": "2024-10-07 20:47:56.411147", - "last_modified_date": "2024-10-21 16:32:58.411000", - "version": 1, - "url": "https://ge.xhamster.com/videos/fucking-at-work-and-with-friend-and-boss-10788692", - "review": 0, - "should_download": 0, - "title": "Ficken bei der Arbeit und mit Freund und Chef | xHamster", - "file_name": "Ficken bei der Arbeit und mit Freund und Chef [10788692].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/daf8430b-71c4-4658-a070-018178829033.mp4" - }, - { - "id": "db03311d-5e61-4075-ba5a-fc10c5fcd70e", - "created_date": "2024-09-24 08:11:39.001434", - "last_modified_date": "2024-10-21 16:33:04.074000", - "version": 1, - "url": "https://ge.xhamster.com/videos/horny-stepmother-jerks-off-her-husbands-dick-while-her-stepson-fucks-her-in-the-ass-with-his-big-dick-xhf533Q", - "review": 0, - "should_download": 0, - "title": "Geile stiefmutter wichst den schwanz ihres ehemanns, w\u00e4hrend ihr stiefsohn sie mit seinem gro\u00dfen schwanz in den arsch fickt! | xHamster", - "file_name": "Geile stiefmutter wichst den schwanz ihres ehemanns, w\u00e4hrend ihr stiefsohn sie mit seinem gro\u00dfen schwanz in den arsch fickt! [xhf533Q].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/db03311d-5e61-4075-ba5a-fc10c5fcd70e.mp4" - }, - { - "id": "db07f91f-39a1-46ad-8316-7274c14ffdd2", - "created_date": "2024-07-25 07:29:46.983251", - "last_modified_date": "2024-07-25 07:29:46.983251", - "version": 0, - "url": "https://ge.xhamster.com/videos/german-redhead-college-girl-lia-louise-picked-up-for-public-sex-xh02h7A", - "review": 0, - "should_download": 0, - "title": "Deutsche rothaarige Sch\u00fclerin Lia Louise im Wald bei Bochum versaut gefickt | xHamster", - "file_name": "Deutsche rothaarige Sch\u00fclerin Lia Louise im Wald bei Bochum versaut gefickt [xh02h7A].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/db07f91f-39a1-46ad-8316-7274c14ffdd2.mp4" - }, - { - "id": "db3595bd-297f-4aed-bca9-f814cc7c5af3", - "created_date": "2024-07-25 07:29:48.082177", - "last_modified_date": "2024-07-25 07:29:48.082177", - "version": 0, - "url": "https://ge.xhamster.com/videos/fotzenheilanstalt-full-movie-feat-steffi-cerien-5593882", - "review": 0, - "should_download": 0, - "title": "Fotzenheilanstalt Full Movie Feat Steffi & Cerien: Porn 1a | xHamster", - "file_name": "Fotzenheilanstalt. (full movie) feat. Steffi & Cerien [5593882].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/db3595bd-297f-4aed-bca9-f814cc7c5af3.mp4" - }, - { - "id": "dbcd6cfb-956f-48dd-982f-e93f50ee6327", - "created_date": "2024-07-25 07:29:44.469296", - "last_modified_date": "2024-07-25 07:29:44.469296", - "version": 0, - "url": "https://ge.xhamster.com/videos/amber-stark-tells-stepbro-you-better-not-cum-in-my-sticky-panties-s7-e7-xhjaKgI", - "review": 0, - "should_download": 0, - "title": "Amber Stark sagt zu Stiefbruder: \"Du solltest besser nicht in mein klebriges H\u00f6schen kommen\" - s7: e7 | xHamster", - "file_name": "Amber Stark sagt zu Stiefbruder\uff1a \uff02Du solltest besser nicht in mein klebriges H\u00f6schen kommen\uff02 - s7\uff1a e7 [xhjaKgI].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/dbcd6cfb-956f-48dd-982f-e93f50ee6327.mp4" - }, - { - "id": "dc2a3f3b-d6e6-419b-a15b-567028929c2e", - "created_date": "2024-08-09 21:20:39.323741", - "last_modified_date": "2024-08-16 10:32:18.780000", - "version": 1, - "url": "https://ge.xhamster.com/videos/blonde-slut-just-needs-cum-on-her-face-xhYRyTL", - "review": 0, - "should_download": 0, - "title": "Blonde Schlampe braucht einfach Sperma im Gesicht !! | xHamster", - "file_name": "Blonde Schlampe braucht einfach Sperma im Gesicht !! [xhYRyTL].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/dc2a3f3b-d6e6-419b-a15b-567028929c2e.mp4" - }, - { - "id": "dc2eccb3-3f30-4c1e-9791-157aa5d30c70", - "created_date": "2025-01-16 19:59:57.271119", - "last_modified_date": "2025-01-16 19:59:57.271126", - "version": 0, - "url": "https://ge.xhamster.com/videos/best-threesome-double-face-sitting-pussy-licking-and-explosive-climaxes-xhDgb4c", - "review": 0, - "should_download": 0, - "title": "Bester dreier: Doppel facesitting, muschi lecken und explosive H\u00f6hepunkte | xHamster", - "file_name": "Bester dreier\uff1a Doppel facesitting, muschi lecken und explosive H\u00f6hepunkte [xhDgb4c].mp4", - "path": null, - "cloud_link": "/data/media/dc2eccb3-3f30-4c1e-9791-157aa5d30c70.mp4" - }, - { - "id": "dc30a728-43cb-43bc-9c9a-0877add4896d", - "created_date": "2024-07-25 07:29:47.480541", - "last_modified_date": "2024-07-25 07:29:47.480541", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-nymph-gf-and-her-naughty-stepmom-both-fucked-doggystyle-xh8mrUf", - "review": 0, - "should_download": 0, - "title": "Meine Nymphomanin und ihre freche Stiefmutter werden beide im Doggystyle gefickt | xHamster", - "file_name": "Meine Nymphomanin und ihre freche Stiefmutter werden beide im Doggystyle gefickt [xh8mrUf].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/dc30a728-43cb-43bc-9c9a-0877add4896d.mp4" - }, - { - "id": "dc33c35b-0aed-4ad4-b469-2d56d8c4c138", - "created_date": "2024-07-25 07:29:46.056997", - "last_modified_date": "2024-07-25 07:29:46.056997", - "version": 0, - "url": "https://ge.xhamster.com/videos/perving-on-my-friend-again-no-im-just-making-a-sandwich-13460999", - "review": 0, - "should_download": 0, - "title": "Geiferst du wieder meiner Freundin nach? Nein, ich mache nur ein Sandwich | xHamster", - "file_name": "Geiferst du wieder meiner Freundin nach\uff1f Nein, ich mache nur ein Sandwich [13460999].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/dc33c35b-0aed-4ad4-b469-2d56d8c4c138.mp4" - }, - { - "id": "dcaa919a-9011-4b0c-8698-e41cd9dfb0ce", - "created_date": "2024-07-25 07:29:46.854606", - "last_modified_date": "2024-07-25 07:29:46.854606", - "version": 0, - "url": "https://ge.xhamster.com/videos/die-nichten-der-frau-oberst-1968-german-soft-full-2k-xhc1vdT", - "review": 0, - "should_download": 0, - "title": "Die Nichten Der Frau Oberst 1968 German Soft Full 2k | xHamster", - "file_name": "Die Nichten der Frau Oberst (1968, German soft, full, 2K) [xhc1vdT].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/dcaa919a-9011-4b0c-8698-e41cd9dfb0ce.mp4" - }, - { - "id": "dcaeafae-9a49-4711-b08a-dc6f9422c00b", - "created_date": "2024-07-25 07:29:44.722230", - "last_modified_date": "2024-07-25 07:29:44.722230", - "version": 0, - "url": "https://ge.xhamster.com/videos/school-class-10581008", - "review": 0, - "should_download": 0, - "title": "Schulklasse | xHamster", - "file_name": "Schulklasse [10581008].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/dcaeafae-9a49-4711-b08a-dc6f9422c00b.mp4" - }, - { - "id": "dcb805b6-61a2-4e1d-a49d-3a0539391134", - "created_date": "2024-07-25 07:29:45.621368", - "last_modified_date": "2024-07-25 07:29:45.621368", - "version": 0, - "url": "https://ge.xhamster.com/videos/inz-mach-mit-12096154", - "review": 0, - "should_download": 0, - "title": "Inz Mach Mit: Free Kissing Porn Video 46 | xHamster", - "file_name": "inz mach mit [12096154].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/dcb805b6-61a2-4e1d-a49d-3a0539391134.mp4" - }, - { - "id": "dcbac5cc-3cd0-4fbb-8b29-4375e3e52384", - "created_date": "2024-07-25 07:29:44.417084", - "last_modified_date": "2024-07-25 07:29:44.417084", - "version": 0, - "url": "https://ge.xhamster.com/videos/zeltplatz-wilde-lust-10882784", - "review": 0, - "should_download": 0, - "title": "Zeltplatz Wilde Lust: Free HD Porn Video 8b | xHamster", - "file_name": "Zeltplatz Wilde Lust [10882784].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/dcbac5cc-3cd0-4fbb-8b29-4375e3e52384.mp4" - }, - { - "id": "dcfee43f-43bb-4875-a996-4701b46e4895", - "created_date": "2025-01-16 20:00:01.035133", - "last_modified_date": "2025-01-16 20:00:01.035140", - "version": 0, - "url": "https://ge.xhamster.com/videos/first-nudity-than-artwork-losing-a-bet-gets-sticky-quick-12126741", - "review": 0, - "should_download": 0, - "title": "Erste Nacktheit als Kunstwerk, der Verlust einer Wette wird schnell klebrig | xHamster", - "file_name": "Erste Nacktheit als Kunstwerk, der Verlust einer Wette wird schnell klebrig [12126741].mp4", - "path": null, - "cloud_link": "/data/media/dcfee43f-43bb-4875-a996-4701b46e4895.mp4" - }, - { - "id": "dd31ba36-a7d5-45a1-ac0c-7e801cd49f68", - "created_date": "2024-10-21 15:08:43.553991", - "last_modified_date": "2024-10-21 16:33:12.056000", - "version": 1, - "url": "https://ge.xhamster.com/videos/more-of-this-old-man-fuck-bitch-w-friend-4909408", - "review": 0, - "should_download": 0, - "title": "Mehr von dieser alten Fick-Schlampe mit Freund | xHamster", - "file_name": "Mehr von dieser alten Fick-Schlampe mit Freund [4909408].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/dd31ba36-a7d5-45a1-ac0c-7e801cd49f68.mp4" - }, - { - "id": "dd3fde41-89cf-4d08-abb7-19d9b56fcc94", - "created_date": "2024-10-07 20:47:56.419311", - "last_modified_date": "2024-10-21 16:33:15.525000", - "version": 1, - "url": "https://ge.xhamster.com/videos/shared-wife-with-friends-10011525", - "review": 0, - "should_download": 0, - "title": "Geteilte Ehefrau mit Freunden | xHamster", - "file_name": "Geteilte Ehefrau mit Freunden [10011525].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/dd3fde41-89cf-4d08-abb7-19d9b56fcc94.mp4" - }, - { - "id": "dd625d42-a7a9-411a-a35f-e7855b322edc", - "created_date": "2024-07-25 07:29:46.264444", - "last_modified_date": "2024-07-25 07:29:46.264444", - "version": 0, - "url": "https://ge.xhamster.com/videos/surprising-my-stepmom-with-an-anal-threesome-tabooheat-xhbUaSP", - "review": 0, - "should_download": 0, - "title": "\u00dcberrascht meine Stiefmutter mit einem analen Dreier! - Tabu-Hitze | xHamster", - "file_name": "\u00dcberrascht meine Stiefmutter mit einem analen Dreier! - Tabu-Hitze [xhbUaSP].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/dd625d42-a7a9-411a-a35f-e7855b322edc.mp4" - }, - { - "id": "dd7652cf-f89f-4764-9d6f-f9cb6dff761c", - "created_date": "2024-08-09 21:34:58.067626", - "last_modified_date": "2024-08-16 10:32:33.303000", - "version": 1, - "url": "https://ge.xhamster.com/videos/secretariat-prive-1980-france-elisabeth-bure-full-movie-xhFvnkF", - "review": 0, - "should_download": 0, - "title": "Secretariat Prive (1980, Frankreich, Elizabeth Bure, kompletter Film) | xHamster", - "file_name": "Secretariat Prive (1980, Frankreich, Elizabeth Bure, kompletter Film) [xhFvnkF].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/dd7652cf-f89f-4764-9d6f-f9cb6dff761c.mp4" - }, - { - "id": "dd7b5467-e944-4766-9cf9-1ad5e804c264", - "created_date": "2024-07-25 07:29:46.390960", - "last_modified_date": "2024-07-25 07:29:46.390960", - "version": 0, - "url": "https://ge.xhamster.com/videos/schulmadchen-10-kleine-rosetten-zaghaft-entjungfert-1994-xhQRCB6", - "review": 0, - "should_download": 0, - "title": "Schulmadchen 10 Kleine Rosetten Zaghaft Entjungfert 1994 | xHamster", - "file_name": "Schulmadchen 10 Kleine Rosetten Zaghaft Entjungfert (1994) [xhQRCB6].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/dd7b5467-e944-4766-9cf9-1ad5e804c264.mp4" - }, - { - "id": "dde0b3aa-6857-41ab-81d0-861c3afb5c69", - "created_date": "2024-09-24 08:11:38.996081", - "last_modified_date": "2024-10-21 16:33:21.158000", - "version": 1, - "url": "https://ge.xhamster.com/videos/lustful-couple-finally-convince-babysitter-to-play-with-them-xhk5RyP", - "review": 0, - "should_download": 0, - "title": "Lustvolles Paar \u00fcberzeugt Babysitter endlich, mit ihnen zu spielen | xHamster", - "file_name": "Lustvolles Paar \u00fcberzeugt Babysitter endlich, mit ihnen zu spielen [xhk5RyP].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/dde0b3aa-6857-41ab-81d0-861c3afb5c69.mp4" - }, - { - "id": "dded0bc8-229a-4433-a997-8f7af361625d", - "created_date": "2024-11-10 16:53:33.478632", - "last_modified_date": "2024-11-10 16:53:33.478632", - "version": 0, - "url": "https://ge.xhamster.com/videos/mai-68-en-69-7862418", - "review": 0, - "should_download": 0, - "title": "Mai 68 En 69: Free Big Tits Porn Video ad | xHamster", - "file_name": "mai 68 en 69 [7862418].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/dded0bc8-229a-4433-a997-8f7af361625d.mp4" - }, - { - "id": "de237a86-309b-4d30-80ed-e1d6f6140e18", - "created_date": "2024-07-25 07:29:44.507124", - "last_modified_date": "2024-07-25 07:29:44.507124", - "version": 0, - "url": "https://ge.xhamster.com/videos/little-18-step-sister-gets-multiple-cumshots-on-her-face-by-her-three-step-brothers-xh4XV7j", - "review": 0, - "should_download": 0, - "title": "Little 18 Step Sister gets Multiple Cumshots on Her Face by Her Three Step Brothers | xHamster", - "file_name": "Little 18+ Step Sister gets Multiple cumshots on her face by her Three Step Brothers [xh4XV7j].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/de237a86-309b-4d30-80ed-e1d6f6140e18.mp4" - }, - { - "id": "de83ba7b-8d81-4a39-b673-6b509da2a982", - "created_date": "2024-07-25 07:29:47.968628", - "last_modified_date": "2024-07-25 07:29:47.968628", - "version": 0, - "url": "https://ge.xhamster.com/videos/brunette-slut-gives-a-wet-blowjob-and-her-wet-pussy-to-a-studs-cock-in-the-pool-xhJmW95", - "review": 0, - "should_download": 0, - "title": "Br\u00fcnette br\u00fcnette schlampe gibt dem schwanz eines hengstes im pool einen nassen blowjob und ihre nasse muschi | xHamster", - "file_name": "Br\u00fcnette br\u00fcnette schlampe gibt dem schwanz eines hengstes im pool einen nassen blowjob und ihre nasse muschi [xhJmW95].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/de83ba7b-8d81-4a39-b673-6b509da2a982.mp4" - }, - { - "id": "deba75a9-f21a-4cd4-a73f-04f18cca16f3", - "created_date": "2025-01-16 19:59:43.332731", - "last_modified_date": "2025-01-16 19:59:43.332738", - "version": 0, - "url": "https://ge.xhamster.com/videos/four-girls-are-enjoying-stripping-in-the-swimming-pool-with-one-lucky-guy-joining-them-xhL4uHO", - "review": 0, - "should_download": 0, - "title": "Vier m\u00e4dchen genie\u00dfen es, im schwimmbad zu, zu denen ein gl\u00fcckspilz geh\u00f6rt | xHamster", - "file_name": "Vier m\u00e4dchen genie\u00dfen es, im schwimmbad zu, zu denen ein gl\u00fcckspilz geh\u00f6rt [xhL4uHO].mp4", - "path": null, - "cloud_link": "/data/media/deba75a9-f21a-4cd4-a73f-04f18cca16f3.mp4" - }, - { - "id": "df260cf5-587e-47ae-a9cb-22fbe9e8729d", - "created_date": "2024-12-29 23:53:27.932560", - "last_modified_date": "2024-12-29 23:53:27.932560", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-family-is-different-xhjwGUC", - "review": 0, - "should_download": 0, - "title": "Meine Familie ist anders | xHamster", - "file_name": "Meine Familie ist anders [xhjwGUC].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/df260cf5-587e-47ae-a9cb-22fbe9e8729d.mp4" - }, - { - "id": "df911d60-3709-44d3-8c7b-b9b2d4a72451", - "created_date": "2024-07-25 07:29:44.435712", - "last_modified_date": "2024-07-25 07:29:44.435712", - "version": 0, - "url": "https://ge.xhamster.com/videos/eine-schrecklich-geile-familie-3-1840791", - "review": 0, - "should_download": 0, - "title": "Eine Schrecklich Geile Familie 3, Free Porn 09 | xHamster", - "file_name": "Eine Schrecklich Geile Familie 3 [1840791].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/df911d60-3709-44d3-8c7b-b9b2d4a72451.mp4" - }, - { - "id": "dfccb432-5002-42cb-a20e-889b4d780ecb", - "created_date": "2024-07-25 07:29:45.749353", - "last_modified_date": "2024-07-25 07:29:45.749353", - "version": 0, - "url": "https://ge.xhamster.com/videos/horny-step-son-fucks-step-mom-sister-dava-foxx-kara-lee-14884943", - "review": 0, - "should_download": 0, - "title": "Geiler Stiefsohn fickt Stiefmutter & Stiefschwester - Dava Foxx & Kara Lee | xHamster", - "file_name": "Geiler Stiefsohn fickt Stiefmutter & Stiefschwester - Dava Foxx & Kara Lee [14884943].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/dfccb432-5002-42cb-a20e-889b4d780ecb.mp4" - }, - { - "id": "e001a4ff-2acf-464f-a44b-904578158c73", - "created_date": "2024-07-25 07:29:44.692168", - "last_modified_date": "2024-07-25 07:29:44.692168", - "version": 0, - "url": "https://ge.xhamster.com/videos/inzt-die-schwestern-sind-los-10538086", - "review": 0, - "should_download": 0, - "title": "Inzt Die Schwestern Sind Los, Free Retro Porn 8a | xHamster", - "file_name": "Inzt Die Schwestern sind los [10538086].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/e001a4ff-2acf-464f-a44b-904578158c73.mp4" - }, - { - "id": "e017f51c-2e56-41a7-8819-791fb26433d6", - "created_date": "2024-07-25 07:29:44.942649", - "last_modified_date": "2024-07-25 07:29:44.942649", - "version": 0, - "url": "https://ge.xhamster.com/videos/german-holiday-group-sex-on-pool-5768372", - "review": 0, - "should_download": 0, - "title": "Deutscher Ferien-Gruppensex am Pool | xHamster", - "file_name": "Deutscher Ferien-Gruppensex am Pool [5768372].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e017f51c-2e56-41a7-8819-791fb26433d6.mp4" - }, - { - "id": "e04b707f-524c-4573-8e3d-40bf13dcfafc", - "created_date": "2024-07-25 07:29:44.981925", - "last_modified_date": "2024-07-25 07:29:44.981925", - "version": 0, - "url": "https://ge.xhamster.com/videos/aria-valencia-starts-a-swap-family-fuck-fest-stuck-in-a-basket-s7-e2-xhhtTAN", - "review": 0, - "should_download": 0, - "title": "Aria Valencia beginnt ein Familien-Fickfest, w\u00e4hrend sie in einem Korb steckt - s7: e2 | xHamster", - "file_name": "Aria Valencia beginnt ein Familien-Fickfest, w\u00e4hrend sie in einem Korb steckt - s7\uff1a e2 [xhhtTAN].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e04b707f-524c-4573-8e3d-40bf13dcfafc.mp4" - }, - { - "id": "e05bcd0d-b49f-4935-8614-0626a3b1472a", - "created_date": "2024-07-25 07:29:46.340349", - "last_modified_date": "2024-07-25 07:29:46.340349", - "version": 0, - "url": "https://ge.xhamster.com/videos/tabulose-familie-without-taboos-xhCRgoB", - "review": 0, - "should_download": 0, - "title": "Tabulose Familie Without Taboos, Free Porn 6f | xHamster", - "file_name": "Tabulose Familie without taboos [xhCRgoB].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e05bcd0d-b49f-4935-8614-0626a3b1472a.mp4" - }, - { - "id": "e0662903-dcac-4c3d-a207-286e54527afb", - "created_date": "2024-07-25 07:29:47.738060", - "last_modified_date": "2024-07-25 07:29:47.738060", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-dick-definitely-wouldnt-fit-in-this-pussy-redneck-fuck-12977014", - "review": 0, - "should_download": 0, - "title": "Mein Schwanz w\u00fcrde definitiv nicht in diesen Muschi-Redneck-Fick passen | xHamster", - "file_name": "Mein Schwanz w\u00fcrde definitiv nicht in diesen Muschi-Redneck-Fick passen [12977014].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e0662903-dcac-4c3d-a207-286e54527afb.mp4" - }, - { - "id": "e09ee61e-55a2-44ee-8b6f-a9d935be421c", - "created_date": "2024-07-25 07:29:46.213232", - "last_modified_date": "2024-07-25 07:29:46.213232", - "version": 0, - "url": "https://ge.xhamster.com/videos/psychology-of-the-orgasm-1970-german-classic-xhLeVL0", - "review": 0, - "should_download": 0, - "title": "Psychologie des Orgasmus (1970) - deutscher Klassiker | xHamster", - "file_name": "Psychologie des Orgasmus (1970) - deutscher Klassiker [xhLeVL0].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e09ee61e-55a2-44ee-8b6f-a9d935be421c.mp4" - }, - { - "id": "e0a77d6e-9f8e-4161-872e-f38b8d5dd78b", - "created_date": "2024-11-10 16:53:33.467595", - "last_modified_date": "2024-11-10 16:53:33.467595", - "version": 0, - "url": "https://ge.xhamster.com/videos/french-holliday-1982-12159589", - "review": 0, - "should_download": 0, - "title": "French Holliday (1982) | xHamster", - "file_name": "French Holliday (1982) [12159589].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/e0a77d6e-9f8e-4161-872e-f38b8d5dd78b.mp4" - }, - { - "id": "e1358a6f-1a63-4aa0-a3e0-45105e2665b4", - "created_date": "2024-07-25 07:29:44.307766", - "last_modified_date": "2024-07-25 07:29:44.307766", - "version": 0, - "url": "https://ge.xhamster.com/videos/insatiable-ep-02-xhFJKYl", - "review": 0, - "should_download": 0, - "title": "Uners\u00e4ttlich Ep 02 | xHamster", - "file_name": "Uners\u00e4ttlich Ep 02 [xhFJKYl].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e1358a6f-1a63-4aa0-a3e0-45105e2665b4.mp4" - }, - { - "id": "e15bfccb-1530-4904-b83e-e5ba1c9530fe", - "created_date": "2024-07-25 07:29:44.990117", - "last_modified_date": "2024-07-25 07:29:44.990117", - "version": 0, - "url": "https://ge.xhamster.com/videos/spicy-roulette-video-with-3-teens-fucking-6366442", - "review": 0, - "should_download": 0, - "title": "W\u00fcrziges Roulette-Video mit 3 Teenagern, die ficken | xHamster", - "file_name": "W\u00fcrziges Roulette-Video mit 3 Teenagern, die ficken [6366442].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e15bfccb-1530-4904-b83e-e5ba1c9530fe.mp4" - }, - { - "id": "e1a0a601-2d74-47a6-9a0b-6a2821d06a99", - "created_date": "2024-07-25 07:29:46.060581", - "last_modified_date": "2024-07-25 07:29:46.060581", - "version": 0, - "url": "https://ge.xhamster.com/videos/private-scandals-3-full-movie-xh4ZBPi", - "review": 0, - "should_download": 0, - "title": "Private Skandale 3 (kompletter Film) | xHamster", - "file_name": "Private Skandale 3 (kompletter Film) [xh4ZBPi].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e1a0a601-2d74-47a6-9a0b-6a2821d06a99.mp4" - }, - { - "id": "e1b6cff6-844a-4024-a1f7-f44d872bc891", - "created_date": "2024-12-30 18:57:55.730000", - "last_modified_date": "2025-01-03 01:46:07.676000", - "version": 2, - "url": "https://ge.xhamster.com/videos/18-videoz-izi-ashley-team-fuck-with-anal-and-facial-10493078", - "review": 0, - "should_download": 0, - "title": "18 Videoz - Izi Ashley - Team-Fick mit Anal und Gesichtsbesamung | xHamster", - "file_name": "18 Videoz - Izi Ashley - Team-Fick mit Anal und Gesichtsbesamung [10493078].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/e1b6cff6-844a-4024-a1f7-f44d872bc891.mp4" - }, - { - "id": "e221801a-7c00-4c48-98b3-2b538a6799b6", - "created_date": "2024-12-29 23:53:27.955457", - "last_modified_date": "2024-12-29 23:53:27.955457", - "version": 0, - "url": "https://ge.xhamster.com/videos/stiff-competition-1984-9264206", - "review": 0, - "should_download": 0, - "title": "Harter Wettbewerb (1984) | xHamster", - "file_name": "Harter Wettbewerb (1984) [9264206].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/e221801a-7c00-4c48-98b3-2b538a6799b6.mp4" - }, - { - "id": "e2702fcd-a125-412a-a7b4-7e20999a67ed", - "created_date": "2024-07-25 07:29:46.877857", - "last_modified_date": "2024-07-25 07:29:46.877857", - "version": 0, - "url": "https://ge.xhamster.com/videos/step-father-step-son-fuck-teen-daughter-family-therapy-xhaDcLo", - "review": 0, - "should_download": 0, - "title": "Stiefvater & Stiefsohn ficken Teen-Tochter - Familientherapie | xHamster", - "file_name": "Stiefvater & Stiefsohn ficken Teen-Tochter - Familientherapie [xhaDcLo].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e2702fcd-a125-412a-a7b4-7e20999a67ed.mp4" - }, - { - "id": "e28e27df-08cb-47ba-abb7-8eb2fba5795d", - "created_date": "2024-07-25 07:29:47.066989", - "last_modified_date": "2024-07-25 07:29:47.066989", - "version": 0, - "url": "https://ge.xhamster.com/videos/nude-pool-party-at-villa-in-pattaya-amateur-russian-couple-xhyZzL8", - "review": 0, - "should_download": 0, - "title": "Nackte Poolparty in der Villa in Pattaya - russisches amateur-paar | xHamster", - "file_name": "Nackte Poolparty in der Villa in Pattaya - russisches amateur-paar [xhyZzL8].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e28e27df-08cb-47ba-abb7-8eb2fba5795d.mp4" - }, - { - "id": "e2923a57-1772-4ac6-86bd-92a16098ad8b", - "created_date": "2024-07-25 07:29:47.729597", - "last_modified_date": "2024-07-25 07:29:47.729597", - "version": 0, - "url": "https://ge.xhamster.com/videos/best-fucking-vacation-ever-xhMXVjz", - "review": 0, - "should_download": 0, - "title": "Bester Fickurlaub aller Zeiten! | xHamster", - "file_name": "Bester Fickurlaub aller Zeiten! [xhMXVjz].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e2923a57-1772-4ac6-86bd-92a16098ad8b.mp4" - }, - { - "id": "e312cb97-6b0d-4c0a-a027-0263b8df0c8a", - "created_date": "2024-07-25 07:29:46.791938", - "last_modified_date": "2024-07-25 07:29:46.791938", - "version": 0, - "url": "https://ge.xhamster.com/videos/pool-party-turns-into-an-orgy-party-f70-120350", - "review": 0, - "should_download": 0, - "title": "Pool-Party wird zu einer Orgie-Party! f70 | xHamster", - "file_name": "Pool-Party wird zu einer Orgie-Party! f70 [120350].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e312cb97-6b0d-4c0a-a027-0263b8df0c8a.mp4" - }, - { - "id": "e318c11b-19ef-49cc-b435-b444214d5f74", - "created_date": "2024-07-25 07:29:47.686525", - "last_modified_date": "2024-07-25 07:29:47.686525", - "version": 0, - "url": "https://ge.xhamster.com/videos/busty-amateur-wife-gets-gangbanged-by-husbands-friends-14757457", - "review": 0, - "should_download": 0, - "title": "Vollbusige Amateur-Ehefrau wird von den Freunden des Ehemanns im Gangbang gefickt | xHamster", - "file_name": "Vollbusige Amateur-Ehefrau wird von den Freunden des Ehemanns im Gangbang gefickt [14757457].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e318c11b-19ef-49cc-b435-b444214d5f74.mp4" - }, - { - "id": "e32badf9-3c89-4652-a73d-984135ab88b1", - "created_date": "2024-08-28 23:21:54.367087", - "last_modified_date": "2024-08-28 23:21:54.367087", - "version": 0, - "url": "https://ge.xhamster.com/videos/lacy-lennons-first-lesbian-fisting-summer-camp-experience-xhqzGqO", - "review": 0, - "should_download": 0, - "title": "Lacy Lennons erstes lesbisches Fisting-Sommercamp-Erlebnis | xHamster", - "file_name": "Lacy Lennons erstes lesbisches Fisting-Sommercamp-Erlebnis [xhqzGqO].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/e32badf9-3c89-4652-a73d-984135ab88b1.mp4" - }, - { - "id": "e3c206f7-6208-472d-8d08-e716b1f366db", - "created_date": "2024-07-25 07:29:44.300116", - "last_modified_date": "2024-07-25 07:29:44.300116", - "version": 0, - "url": "https://ge.xhamster.com/videos/puremature-seductive-step-mom-alison-star-gets-banged-on-romantic-1291641", - "review": 0, - "should_download": 0, - "title": "Puremature - verf\u00fchrerische Stiefmutter Alison Star wird romantisch geknallt | xHamster", - "file_name": "Puremature - verf\u00fchrerische Stiefmutter Alison Star wird romantisch geknallt [1291641].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e3c206f7-6208-472d-8d08-e716b1f366db.mp4" - }, - { - "id": "e3c4ee26-a23b-4f49-b9cb-3354f2175dac", - "created_date": "2024-07-25 07:29:48.094331", - "last_modified_date": "2024-07-25 07:29:48.094331", - "version": 0, - "url": "https://ge.xhamster.com/videos/strand-orgie-1993-xhXGl0Xv", - "review": 0, - "should_download": 0, - "title": "Strand Orgie 1993: Free European Porn Video 2b | xHamster", - "file_name": "Strand Orgie (1993) [xhXGl0Xv].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e3c4ee26-a23b-4f49-b9cb-3354f2175dac.mp4" - }, - { - "id": "e4452278-d7a3-4aa1-bbe3-dbd55346ee1c", - "created_date": "2024-07-25 07:29:46.437541", - "last_modified_date": "2024-07-25 07:29:46.437541", - "version": 0, - "url": "https://ge.xhamster.com/videos/newly-hatched-chicks-are-ready-to-be-fucked-13508686", - "review": 0, - "should_download": 0, - "title": "Die megageile K\u00fcken-Farm | xHamster", - "file_name": "Die megageile K\u00fcken-Farm [13508686].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e4452278-d7a3-4aa1-bbe3-dbd55346ee1c.mp4" - }, - { - "id": "e47c4efd-004c-49f2-9058-ff07524026f5", - "created_date": "2024-07-25 07:29:44.574377", - "last_modified_date": "2024-07-25 07:29:44.574377", - "version": 0, - "url": "https://ge.xhamster.com/videos/best-friends-are-fucking-for-first-time-after-wife-s-approval-xhtnL0F", - "review": 0, - "should_download": 0, - "title": "Beste Freundinnen ficken zum ersten Mal nach Zustimmung der Ehefrau | xHamster", - "file_name": "Beste Freundinnen ficken zum ersten Mal nach Zustimmung der Ehefrau [xhtnL0F].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e47c4efd-004c-49f2-9058-ff07524026f5.mp4" - }, - { - "id": "e4a850aa-64fc-44e6-8793-b6a058f3313c", - "created_date": "2024-07-25 07:29:45.218685", - "last_modified_date": "2024-07-25 07:29:45.218685", - "version": 0, - "url": "https://ge.xhamster.com/videos/6-hot-girls-in-party-game-341932", - "review": 0, - "should_download": 0, - "title": "6 hei\u00dfe M\u00e4dchen im Partyspiel | xHamster", - "file_name": "6 hei\u00dfe M\u00e4dchen im Partyspiel [341932].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e4a850aa-64fc-44e6-8793-b6a058f3313c.mp4" - }, - { - "id": "e502ee3c-7a8f-490a-9f08-5d506d37068b", - "created_date": "2024-07-25 07:29:45.188855", - "last_modified_date": "2024-07-25 07:29:45.188855", - "version": 0, - "url": "https://ge.xhamster.com/videos/itll-get-much-bigger-if-you-tug-on-it-xhAmxS5", - "review": 0, - "should_download": 0, - "title": "Es wird viel gr\u00f6\u00dfer, wenn du daran ziehst! | xHamster", - "file_name": "Es wird viel gr\u00f6\u00dfer, wenn du daran ziehst! [xhAmxS5].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e502ee3c-7a8f-490a-9f08-5d506d37068b.mp4" - }, - { - "id": "e561b190-63f6-48eb-95db-d81279d16bb6", - "created_date": "2024-07-25 07:29:45.509393", - "last_modified_date": "2024-07-25 07:29:45.509393", - "version": 0, - "url": "https://ge.xhamster.com/videos/among-the-greatest-porn-films-ever-made-179-15006023", - "review": 0, - "should_download": 0, - "title": "Einer der gr\u00f6\u00dften Pornofilme aller Zeiten 179 | xHamster", - "file_name": "Einer der gr\u00f6\u00dften Pornofilme aller Zeiten 179 [15006023].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/e561b190-63f6-48eb-95db-d81279d16bb6.mp4" - }, - { - "id": "e56bbdfe-fa48-4756-b1f5-aaeb34185468", - "created_date": "2024-07-25 07:29:47.581853", - "last_modified_date": "2024-07-25 07:29:47.581853", - "version": 0, - "url": "https://ge.xhamster.com/videos/die-unbeugsame-xh9TsYf", - "review": 0, - "should_download": 0, - "title": "Das Unbemiteliche | xHamster", - "file_name": "Das Unbemiteliche [xh9TsYf].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e56bbdfe-fa48-4756-b1f5-aaeb34185468.mp4" - }, - { - "id": "e56ebcd3-df33-4f00-95fb-d0cd7fc1fdfc", - "created_date": "2024-07-25 07:29:46.833603", - "last_modified_date": "2024-07-25 07:29:46.833603", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-girlfriends-dad-fucked-me-hard-and-came-in-my-ass-to-teach-me-how-to-do-it-with-my-boyfriend-xhSs2E5", - "review": 0, - "should_download": 0, - "title": "Vater der freundin fickte sie hart und kam in ihren arsch, um mich zu lehren, wie man es mit ihrem freund macht. | xHamster", - "file_name": "Vater der freundin fickte sie hart und kam in ihren arsch, um mich zu lehren, wie man es mit ihrem freund macht. [xhSs2E5].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e56ebcd3-df33-4f00-95fb-d0cd7fc1fdfc.mp4" - }, - { - "id": "e57df7ea-3a47-48e5-90a0-e206f3aa398e", - "created_date": "2024-07-25 07:29:46.561021", - "last_modified_date": "2024-07-25 07:29:46.561021", - "version": 0, - "url": "https://ge.xhamster.com/videos/big-titty-18-year-old-fresh-teen-vs-college-thick-cock-bro-9306643", - "review": 0, - "should_download": 0, - "title": "Dicke Titten, 18 Jahre alt, frisches Teen gegen College, dicker Schwanz, Bruder | xHamster", - "file_name": "Dicke Titten, 18 Jahre alt, frisches Teen gegen College, dicker Schwanz, Bruder [9306643].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e57df7ea-3a47-48e5-90a0-e206f3aa398e.mp4" - }, - { - "id": "e59406ee-502a-489d-a8ca-e44e9839a556", - "created_date": "2024-07-25 07:29:44.495727", - "last_modified_date": "2024-07-25 07:29:44.495727", - "version": 0, - "url": "https://ge.xhamster.com/videos/foxy-lady-11-1988-germany-english-live-sound-full-hd-xhMfNUr", - "review": 0, - "should_download": 0, - "title": "Foxy Lady # 11 (1988, Deutschland, englischer Live-Sound, voll, hd) | xHamster", - "file_name": "Foxy Lady # 11 (1988, Deutschland, englischer Live-Sound, voll, hd) [xhMfNUr].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e59406ee-502a-489d-a8ca-e44e9839a556.mp4" - }, - { - "id": "e5a0bd24-d317-4446-b0bb-77185b2f6239", - "created_date": "2024-08-16 12:20:55.081000", - "last_modified_date": "2024-08-16 12:20:55.081000", - "version": 0, - "url": "https://ge.xhamster.com/videos/mom-step-dad-daughter-and-boy-friend-12581760", - "review": 0, - "should_download": 0, - "title": "Mutter, Stiefvater, Tochter und Freund | xHamster", - "file_name": "Mutter, Stiefvater, Tochter und Freund [12581760].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/e5a0bd24-d317-4446-b0bb-77185b2f6239.mp4" - }, - { - "id": "e5acacad-9ccf-4e63-a125-65745401229a", - "created_date": "2024-12-29 23:53:27.902052", - "last_modified_date": "2024-12-29 23:53:27.902052", - "version": 0, - "url": "https://ge.xhamster.com/videos/petite-tattooed-babe-fucked-in-the-office-xh18KRX", - "review": 0, - "should_download": 0, - "title": "Zierliches t\u00e4towiertes sch\u00e4tzchen im b\u00fcro gefickt | xHamster", - "file_name": "Zierliches t\u00e4towiertes sch\u00e4tzchen im b\u00fcro gefickt [xh18KRX].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/e5acacad-9ccf-4e63-a125-65745401229a.mp4" - }, - { - "id": "e5b774e7-3ce7-4032-b268-be1a5adda4e1", - "created_date": "2024-11-10 16:53:33.475766", - "last_modified_date": "2024-11-10 16:53:33.475766", - "version": 0, - "url": "https://ge.xhamster.com/videos/i-am-always-ready-for-group-sex-1978-13175752", - "review": 0, - "should_download": 0, - "title": "Ich bin immer bereit f\u00fcr Gruppensex (1978) | xHamster", - "file_name": "Ich bin immer bereit f\u00fcr Gruppensex (1978) [13175752].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/e5b774e7-3ce7-4032-b268-be1a5adda4e1.mp4" - }, - { - "id": "e5bfbada-d059-4488-b043-98f3478a8a70", - "created_date": "2024-07-25 07:29:45.737577", - "last_modified_date": "2024-07-25 07:29:45.737577", - "version": 0, - "url": "https://ge.xhamster.com/videos/familystrokes-perv-stepmom-and-stepdad-expose-their-naughty-stepdaughter-jessae-rosaes-sex-secret-xhNjs2X", - "review": 0, - "should_download": 0, - "title": "FamilyStrokes - perverse stiefmutter und stiefvater entlarven das sexgeheimnis ihrer frechen stieftochter Jessae Rosae | xHamster", - "file_name": "FamilyStrokes - perverse stiefmutter und stiefvater entlarven das sexgeheimnis ihrer frechen stieftochter Jessae Rosae [xhNjs2X].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/e5bfbada-d059-4488-b043-98f3478a8a70.mp4" - }, - { - "id": "e5c4b753-660e-4c61-9ba1-90bb9fe36e71", - "created_date": "2024-07-25 07:29:45.267345", - "last_modified_date": "2024-07-25 07:29:45.267345", - "version": 0, - "url": "https://ge.xhamster.com/videos/freeuse-new-years-eve-sex-party-teamskeet-xhUnr5O", - "review": 0, - "should_download": 0, - "title": "FreeUse silvester-sex-party - teamSkeet | xHamster", - "file_name": "FreeUse silvester-sex-party - teamSkeet [xhUnr5O].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e5c4b753-660e-4c61-9ba1-90bb9fe36e71.mp4" - }, - { - "id": "e616807c-b138-4341-83ff-a9cb0b320f3f", - "created_date": "2024-07-25 07:29:47.051272", - "last_modified_date": "2024-07-25 07:29:47.051272", - "version": 0, - "url": "https://ge.xhamster.com/videos/outdoor-family-therapy-groupsex-orgy-13987544", - "review": 0, - "should_download": 0, - "title": "Outdoor-Familientherapie-Gruppensex-Orgie | xHamster", - "file_name": "Outdoor-Familientherapie-Gruppensex-Orgie [13987544].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e616807c-b138-4341-83ff-a9cb0b320f3f.mp4" - }, - { - "id": "e6519025-f618-4e65-92f1-1302cb2a5c98", - "created_date": "2024-07-25 07:29:46.710487", - "last_modified_date": "2024-07-25 07:29:46.710487", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepbrother-entertain-me-with-outdoor-anal-sex-by-the-pool-xhtuzIs", - "review": 0, - "should_download": 0, - "title": "Stiefbruer, unterhalten sie mich mit analsex im freien am pool. | xHamster", - "file_name": "Stiefbruer, unterhalten sie mich mit analsex im freien am pool. [xhtuzIs].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e6519025-f618-4e65-92f1-1302cb2a5c98.mp4" - }, - { - "id": "e664a34c-280e-4a30-949e-583b990e7cf7", - "created_date": "2024-07-25 07:29:45.378839", - "last_modified_date": "2024-07-25 07:29:45.378839", - "version": 0, - "url": "https://ge.xhamster.com/videos/redhead-stepsister-gives-an-amazing-blowjob-in-pov-14091400", - "review": 0, - "should_download": 0, - "title": "Rothaarige Stiefschwester gibt einen erstaunlichen Blowjob in POV | xHamster", - "file_name": "Rothaarige Stiefschwester gibt einen erstaunlichen Blowjob in POV [14091400].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e664a34c-280e-4a30-949e-583b990e7cf7.mp4" - }, - { - "id": "e6cd0cd7-6f15-4a91-be5b-63d9033c4e60", - "created_date": "2024-07-25 07:29:46.844974", - "last_modified_date": "2024-07-25 07:29:46.844974", - "version": 0, - "url": "https://ge.xhamster.com/videos/junge-schlampen-auf-der-suche-nach-orgasmus-full-movie-xhu6G2x", - "review": 0, - "should_download": 0, - "title": "Junge Schlampen Auf Der Suche Nach Orgasmus Full Movie | xHamster", - "file_name": "Junge Schlampen auf der Suche nach Orgasmus (Full Movie) [xhu6G2x].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e6cd0cd7-6f15-4a91-be5b-63d9033c4e60.mp4" - }, - { - "id": "e6cd3333-c84b-4594-a744-3fa6eb676810", - "created_date": "2024-07-25 07:29:47.393574", - "last_modified_date": "2024-07-25 07:29:47.393574", - "version": 0, - "url": "https://ge.xhamster.com/videos/wild-weekend-14627578", - "review": 0, - "should_download": 0, - "title": "Wildes Wochenende | xHamster", - "file_name": "Wildes Wochenende [14627578].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e6cd3333-c84b-4594-a744-3fa6eb676810.mp4" - }, - { - "id": "e6d72b5f-eb2d-4446-a0c5-be96c49b685c", - "created_date": "2024-07-25 07:29:44.868766", - "last_modified_date": "2024-07-25 07:29:44.868766", - "version": 0, - "url": "https://ge.xhamster.com/videos/husband-share-wife-12126645", - "review": 0, - "should_download": 0, - "title": "Ehemann teilt Ehefrau | xHamster", - "file_name": "Ehemann teilt Ehefrau [12126645].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e6d72b5f-eb2d-4446-a0c5-be96c49b685c.mp4" - }, - { - "id": "e6dfc9c5-de92-4930-8e96-327cb1f51b31", - "created_date": "2024-07-25 07:29:47.217276", - "last_modified_date": "2024-07-25 07:29:47.217276", - "version": 0, - "url": "https://ge.xhamster.com/videos/meine-stiefsohnreaktion-wenn-ich-kein-hoeschen-trage566-xhokghx", - "review": 0, - "should_download": 0, - "title": "Meine Stiefsohnreaktion Wenn Ich Kein Hoeschen Trage566 | xHamster", - "file_name": "Meine Stiefsohnreaktion, wenn ich kein Hoeschen trage566 [xhokghx].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e6dfc9c5-de92-4930-8e96-327cb1f51b31.mp4" - }, - { - "id": "e6f18f85-6ce2-4c20-9668-a58e88eab91b", - "created_date": "2024-07-25 07:29:45.329462", - "last_modified_date": "2024-07-25 07:29:45.329462", - "version": 0, - "url": "https://ge.xhamster.com/videos/it-s-so-big-new-potential-roommate-catches-roomy-masturbating-to-porn-xhvkgSg", - "review": 0, - "should_download": 0, - "title": "Es ist so gro\u00df! neuer potenzieller Mitbewohner erwischt ger\u00e4umiges Masturbieren zum Porno | xHamster", - "file_name": "Es ist so gro\u00df! neuer potenzieller Mitbewohner erwischt ger\u00e4umiges Masturbieren zum Porno [xhvkgSg].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/e6f18f85-6ce2-4c20-9668-a58e88eab91b.mp4" - }, - { - "id": "e6fe3c76-6f00-4325-bd13-84bbfaf04242", - "created_date": "2024-07-25 07:29:46.344442", - "last_modified_date": "2024-07-25 07:29:46.344442", - "version": 0, - "url": "https://ge.xhamster.com/videos/class-orgy-7012854", - "review": 0, - "should_download": 0, - "title": "Klassenorgie. | xHamster", - "file_name": "Klassenorgie. [7012854].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e6fe3c76-6f00-4325-bd13-84bbfaf04242.mp4" - }, - { - "id": "e7940f48-a47e-4be7-9534-b374138bde94", - "created_date": "2024-08-28 23:21:54.372638", - "last_modified_date": "2024-08-28 23:21:54.372638", - "version": 0, - "url": "https://ge.xhamster.com/videos/agent-aika-4-ova-anime-1997-xhcYkOW", - "review": 0, - "should_download": 0, - "title": "Agent Aika 4 Ova Anime 1997, Free Comic Porn e0 | xHamster", - "file_name": "Agent Aika #4 OVA anime (1997) [xhcYkOW].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/e7940f48-a47e-4be7-9534-b374138bde94.mp4" - }, - { - "id": "e7af56ac-feb4-4a62-b667-192eba01e3c3", - "created_date": "2024-07-25 07:29:48.075053", - "last_modified_date": "2024-07-25 07:29:48.075053", - "version": 0, - "url": "https://ge.xhamster.com/videos/sexual-heights-1979-12884357", - "review": 0, - "should_download": 0, - "title": "Sexuelle H\u00f6hen (1979) | xHamster", - "file_name": "Sexuelle H\u00f6hen (1979) [12884357].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e7af56ac-feb4-4a62-b667-192eba01e3c3.mp4" - }, - { - "id": "e7d82f2f-4019-4a88-83ec-88394ecf96a6", - "created_date": "2024-10-21 15:08:43.558135", - "last_modified_date": "2024-10-21 16:33:28.962000", - "version": 1, - "url": "https://ge.xhamster.com/videos/drncm-classic-dp-g23-xhkz13C", - "review": 0, - "should_download": 0, - "title": "Drncm Klassiker, Doppelpenetration G23 | xHamster", - "file_name": "Drncm Klassiker, Doppelpenetration G23 [xhkz13C].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/e7d82f2f-4019-4a88-83ec-88394ecf96a6.mp4" - }, - { - "id": "e8087c24-3923-437d-a446-ab8ce23b8579", - "created_date": "2024-07-25 07:29:48.129263", - "last_modified_date": "2024-07-25 07:29:48.129263", - "version": 0, - "url": "https://ge.xhamster.com/videos/durchgefickt-und-abgesaugt-6790032", - "review": 0, - "should_download": 0, - "title": "Durchgefickt Und Abgesaugt, Free Anal Porn 4b | xHamster", - "file_name": "Durchgefickt Und Abgesaugt [6790032].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e8087c24-3923-437d-a446-ab8ce23b8579.mp4" - }, - { - "id": "e822d1b7-c689-4584-acb1-5217ead4ccf7", - "created_date": "2025-01-16 19:59:35.850005", - "last_modified_date": "2025-01-16 19:59:35.850011", - "version": 0, - "url": "https://ge.xhamster.com/videos/two-guys-and-two-girls-play-highest-card-wins-4155434", - "review": 0, - "should_download": 0, - "title": "Zwei Typen und zwei M\u00e4dchen spielen mit den h\u00f6chsten Kartensiegen | xHamster", - "file_name": "Zwei Typen und zwei M\u00e4dchen spielen mit den h\u00f6chsten Kartensiegen [4155434].mp4", - "path": null, - "cloud_link": "/data/media/e822d1b7-c689-4584-acb1-5217ead4ccf7.mp4" - }, - { - "id": "e883e73a-696d-41cf-b1ca-9a3be09723e1", - "created_date": "2024-07-25 07:29:45.718880", - "last_modified_date": "2024-07-25 07:29:45.718880", - "version": 0, - "url": "https://ge.xhamster.com/videos/deutsche-geilheit-verliebter-frauen-full-movie-xhxmN35", - "review": 0, - "should_download": 0, - "title": "Deutsche Geilheit Verliebter Frauen Full Movie: Porn a1 | xHamster", - "file_name": "Deutsche Geilheit verliebter Frauen (Full Movie) [xhxmN35].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e883e73a-696d-41cf-b1ca-9a3be09723e1.mp4" - }, - { - "id": "e8878e4f-a510-4f50-b8cc-7be06e1b5f5b", - "created_date": "2024-07-25 07:29:47.390141", - "last_modified_date": "2024-07-25 07:29:47.390141", - "version": 0, - "url": "https://ge.xhamster.com/videos/horny-step-siblings-love-masturbating-together-they-get-caught-all-the-time-pervtherapy-xhx6DaxR", - "review": 0, - "should_download": 0, - "title": "Geile stiefgeschwister lieben es, zusammen zu masturbieren und sie werden die ganze zeit erwischt - pervtherapie | xHamster", - "file_name": "Geile stiefgeschwister lieben es, zusammen zu masturbieren und sie werden die ganze zeit erwischt - pervtherapie [xhx6DaxR].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e8878e4f-a510-4f50-b8cc-7be06e1b5f5b.mp4" - }, - { - "id": "e889e5dd-53ca-4669-b60e-bab92f8f448b", - "created_date": "2024-07-25 07:29:47.333679", - "last_modified_date": "2024-07-25 07:29:47.333679", - "version": 0, - "url": "https://ge.xhamster.com/videos/for-the-love-of-pleasure-1979-xhWwBEx", - "review": 0, - "should_download": 0, - "title": "F\u00fcr die Liebe zum Vergn\u00fcgen (1979) | xHamster", - "file_name": "F\u00fcr die Liebe zum Vergn\u00fcgen (1979) [xhWwBEx].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e889e5dd-53ca-4669-b60e-bab92f8f448b.mp4" - }, - { - "id": "e8aa5da4-637e-4fd4-831b-e0b12856f4e0", - "created_date": "2024-07-25 07:29:46.990633", - "last_modified_date": "2024-07-25 07:29:46.990633", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-dirty-stepsister-plays-with-her-wet-pussy-xho05mY", - "review": 0, - "should_download": 0, - "title": "Meine schmutzige stiefschwester spielt mit ihrer nassen muschi | xHamster", - "file_name": "Meine schmutzige stiefschwester spielt mit ihrer nassen muschi [xho05mY].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e8aa5da4-637e-4fd4-831b-e0b12856f4e0.mp4" - }, - { - "id": "e9a0c168-28d6-4b01-96c7-9907bf7e4c36", - "created_date": "2024-07-25 07:29:46.783683", - "last_modified_date": "2024-07-25 07:29:46.783683", - "version": 0, - "url": "https://ge.xhamster.com/videos/redhead-step-sister-s7-e4-xhwmhPN", - "review": 0, - "should_download": 0, - "title": "Rothaarige stiefschweige - s7: e4 | xHamster", - "file_name": "Rothaarige stiefschweige - s7\uff1a e4 [xhwmhPN].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/e9a0c168-28d6-4b01-96c7-9907bf7e4c36.mp4" - }, - { - "id": "e9aaf691-d633-476d-b1f7-0faafe2df694", - "created_date": "2024-10-21 15:08:43.548645", - "last_modified_date": "2024-10-21 16:33:31.842000", - "version": 1, - "url": "https://ge.xhamster.com/videos/sex-in-two-holes-2850403", - "review": 0, - "should_download": 0, - "title": "Sex in zwei L\u00f6chern | xHamster", - "file_name": "Sex in zwei L\u00f6chern [2850403].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/e9aaf691-d633-476d-b1f7-0faafe2df694.mp4" - }, - { - "id": "e9dd2738-805f-4f83-bb7e-4b49b502459e", - "created_date": "2024-10-07 20:47:56.414103", - "last_modified_date": "2024-10-21 16:33:34.913000", - "version": 1, - "url": "https://ge.xhamster.com/videos/short-hair-housewife-doublejob-3416515", - "review": 0, - "should_download": 0, - "title": "Kurze Haare, Hausfrau, Doublejob | xHamster", - "file_name": "Kurze Haare, Hausfrau, Doublejob [3416515].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/e9dd2738-805f-4f83-bb7e-4b49b502459e.mp4" - }, - { - "id": "ead8d58d-1ac0-4c56-8f0e-8ad74b44c403", - "created_date": "2025-01-16 19:59:50.915391", - "last_modified_date": "2025-01-16 19:59:50.915398", - "version": 0, - "url": "https://ge.xhamster.com/videos/sexy-family-xh5FAXZ", - "review": 0, - "should_download": 0, - "title": "Sexy Familie | xHamster", - "file_name": "Sexy Familie [xh5FAXZ].mp4", - "path": null, - "cloud_link": "/data/media/ead8d58d-1ac0-4c56-8f0e-8ad74b44c403.mp4" - }, - { - "id": "eb0d4866-db0f-4a18-a91f-d413d725a7fa", - "created_date": "2024-07-25 07:29:47.494956", - "last_modified_date": "2024-07-25 07:29:47.494956", - "version": 0, - "url": "https://ge.xhamster.com/videos/indecency-1998-special-edition-xhg58Sz", - "review": 0, - "should_download": 0, - "title": "Indecency (1998) Sonderausgabe | xHamster", - "file_name": "Indecency (1998) Sonderausgabe [xhg58Sz].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/eb0d4866-db0f-4a18-a91f-d413d725a7fa.mp4" - }, - { - "id": "eb13a059-a705-4607-a858-12da1da59da0", - "created_date": "2024-07-25 07:29:45.759972", - "last_modified_date": "2024-07-25 07:29:45.759972", - "version": 0, - "url": "https://ge.xhamster.com/videos/mein-verfickter-urlaub-full-movie-xhETPMs", - "review": 0, - "should_download": 0, - "title": "Mein Verfickter Urlaub Full Movie, Free Porn 57 | xHamster", - "file_name": "Mein verfickter Urlaub (Full Movie) [xhETPMs].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/eb13a059-a705-4607-a858-12da1da59da0.mp4" - }, - { - "id": "eb1c5feb-15e5-4b6c-9e1a-ab2c9f9a327d", - "created_date": "2024-07-25 07:29:47.529304", - "last_modified_date": "2024-07-25 07:29:47.529304", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-step-sis-april-fools-prank-makes-me-cum-inside-her-s3-e4-11471257", - "review": 0, - "should_download": 0, - "title": "Meine Stiefschwester April Dummkopf Streich, bringt mich in ihr s3: e4 kommen | xHamster", - "file_name": "Meine Stiefschwester April Dummkopf Streich, bringt mich in ihr s3\uff1a e4 kommen [11471257].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/eb1c5feb-15e5-4b6c-9e1a-ab2c9f9a327d.mp4" - }, - { - "id": "eb88948e-f308-4640-89a0-cb5e12bc6706", - "created_date": "2024-07-25 07:29:44.787627", - "last_modified_date": "2024-07-25 07:29:44.787627", - "version": 0, - "url": "https://ge.xhamster.com/videos/horny-stepdaughter-hides-under-the-blanket-to-be-fucked-by-stepfathers-cock-and-make-a-threesome-with-stepmom-charlie-forde-xhc4G6J", - "review": 0, - "should_download": 0, - "title": "Horny Stepdaughter Hides Under the Blanket to be Fucked by Stepfather's Cock and Make a Threesome with Stepmom Charlie Forde | xHamster", - "file_name": "Horny stepdaughter hides under the blanket to be fucked by stepfather's cock and make a threesome with stepmom Charlie Forde. [xhc4G6J].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/eb88948e-f308-4640-89a0-cb5e12bc6706.mp4" - }, - { - "id": "ebc2aac1-755b-4536-9542-9a3acb00130b", - "created_date": "2024-07-25 07:29:46.149726", - "last_modified_date": "2024-07-25 07:29:46.149726", - "version": 0, - "url": "https://ge.xhamster.com/videos/i-like-to-watch-1982-xhFL5ZA", - "review": 0, - "should_download": 0, - "title": "Ich schaue gerne zu (1982) | xHamster", - "file_name": "Ich schaue gerne zu (1982) [xhFL5ZA].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ebc2aac1-755b-4536-9542-9a3acb00130b.mp4" - }, - { - "id": "ebc5033a-39eb-423f-ac2a-9fab8f287390", - "created_date": "2024-07-25 07:29:45.263590", - "last_modified_date": "2024-07-25 07:29:45.263590", - "version": 0, - "url": "https://ge.xhamster.com/videos/cousin-betty-1972-upscaled-to-4k-xhkLDji", - "review": 0, - "should_download": 0, - "title": "Cousin Betty (1972), auf 4k hochskaliert | xHamster", - "file_name": "Cousin Betty (1972), auf 4k hochskaliert [xhkLDji].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/ebc5033a-39eb-423f-ac2a-9fab8f287390.mp4" - }, - { - "id": "ec41225d-a3e7-4b6a-bc06-5b74eae33a44", - "created_date": "2024-07-25 07:29:45.704473", - "last_modified_date": "2024-07-25 07:29:45.704473", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-friends-hot-mom-is-ava-addams-xhzWc4I", - "review": 0, - "should_download": 0, - "title": "Die hei\u00dfe mutter meines freundes ist Ava addams | xHamster", - "file_name": "Die hei\u00dfe mutter meines freundes ist Ava addams [xhzWc4I].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/ec41225d-a3e7-4b6a-bc06-5b74eae33a44.mp4" - }, - { - "id": "ec459efe-f3df-4b7d-86cf-92a1265e2d8d", - "created_date": "2024-07-25 07:29:47.379520", - "last_modified_date": "2024-07-25 07:29:47.379520", - "version": 0, - "url": "https://ge.xhamster.com/videos/german-vintage-compilation-14508029", - "review": 0, - "should_download": 0, - "title": "Deutsche Retro-Zusammenstellung | xHamster", - "file_name": "Deutsche Retro-Zusammenstellung [14508029].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ec459efe-f3df-4b7d-86cf-92a1265e2d8d.mp4" - }, - { - "id": "ec495eea-2d88-4430-b9f8-47cda588787d", - "created_date": "2024-07-25 07:29:47.100655", - "last_modified_date": "2024-07-25 07:29:47.100655", - "version": 0, - "url": "https://ge.xhamster.com/videos/anal-3some-in-the-office-8086339", - "review": 0, - "should_download": 0, - "title": "Analer Dreier im B\u00fcro | xHamster", - "file_name": "Analer Dreier im B\u00fcro [8086339].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ec495eea-2d88-4430-b9f8-47cda588787d.mp4" - }, - { - "id": "ec7bd3fd-ccd5-48c5-a3dd-3beaf05f8fdb", - "created_date": "2024-09-24 08:11:39.000025", - "last_modified_date": "2024-10-21 16:33:40.600000", - "version": 1, - "url": "https://ge.xhamster.com/videos/virgin-stepson-begs-his-stepmom-to-show-him-how-to-fuck-xh6Wznu", - "review": 0, - "should_download": 0, - "title": "Virgin Stiefsohn fleht seine Stiefmutter an, ihm zu zeigen, wie man fickt | xHamster", - "file_name": "Virgin Stiefsohn fleht seine Stiefmutter an, ihm zu zeigen, wie man fickt [xh6Wznu].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/ec7bd3fd-ccd5-48c5-a3dd-3beaf05f8fdb.mp4" - }, - { - "id": "eca594b8-2982-4a41-b326-b55c0ab2bac5", - "created_date": "2024-07-25 07:29:46.445788", - "last_modified_date": "2024-07-25 07:29:46.445788", - "version": 0, - "url": "https://ge.xhamster.com/videos/jojo-kiss-caught-on-her-boyfriend-and-a-masseuse-9253178", - "review": 0, - "should_download": 0, - "title": "Jojo Kiss k\u00fcsste ihren Freund und eine Masseuse | xHamster", - "file_name": "Jojo Kiss k\u00fcsste ihren Freund und eine Masseuse [9253178].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/eca594b8-2982-4a41-b326-b55c0ab2bac5.mp4" - }, - { - "id": "ecace882-c3c0-4565-8268-15acadb073cf", - "created_date": "2024-07-25 07:29:47.438861", - "last_modified_date": "2024-07-25 07:29:47.438861", - "version": 0, - "url": "https://ge.xhamster.com/videos/naive-and-slutty-office-girl-fucking-the-wrong-cock-xhtd25C", - "review": 0, - "should_download": 0, - "title": "Naives und versautes B\u00fcrom\u00e4dchen fickt den falschen Schwanz | xHamster", - "file_name": "Naives und versautes B\u00fcrom\u00e4dchen fickt den falschen Schwanz [xhtd25C].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ecace882-c3c0-4565-8268-15acadb073cf.mp4" - }, - { - "id": "ecca935f-4bdd-4946-ad4c-695860585c9b", - "created_date": "2024-07-25 07:29:45.249005", - "last_modified_date": "2024-07-25 07:29:45.249005", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepmom-says-youre-having-sex-with-the-sponges-xhoKcsY", - "review": 0, - "should_download": 0, - "title": "Stiefmutter sagt, du hast Sex mit den Schw\u00e4mmen ?! | xHamster", - "file_name": "Stiefmutter sagt, du hast Sex mit den Schw\u00e4mmen \uff1f! [xhoKcsY].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ecca935f-4bdd-4946-ad4c-695860585c9b.mp4" - }, - { - "id": "eceb3620-68fd-4a3c-9928-70c169ce4b3f", - "created_date": "2024-07-25 07:29:44.745680", - "last_modified_date": "2024-07-25 07:29:44.745680", - "version": 0, - "url": "https://ge.xhamster.com/videos/flirt-or-fuck-s41-e13-xhajGiY", - "review": 0, - "should_download": 0, - "title": "Flirten oder Ficken - S41:E13 | xHamster", - "file_name": "Flirten oder Ficken - S41\uff1aE13 [xhajGiY].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/eceb3620-68fd-4a3c-9928-70c169ce4b3f.mp4" - }, - { - "id": "ed22a066-5070-46ab-9e07-c43ebef8d44b", - "created_date": "2024-12-29 23:53:27.960430", - "last_modified_date": "2024-12-29 23:53:27.960430", - "version": 0, - "url": "https://ge.xhamster.com/videos/emmanuel-480064", - "review": 0, - "should_download": 0, - "title": "Emmanuel | xHamster", - "file_name": "Emmanuel [480064].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/ed22a066-5070-46ab-9e07-c43ebef8d44b.mp4" - }, - { - "id": "ed236077-0b3d-4165-a157-0a215ca85663", - "created_date": "2024-12-29 23:53:27.925332", - "last_modified_date": "2024-12-29 23:53:27.925332", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-mischiefs-part-1-step-mom-seduces-her-step-daughters-huge-dick-boyfriend-xhNTlTd", - "review": 0, - "should_download": 0, - "title": "Familien-unfug (teil 1): Stiefmutter verf\u00fchrt den freund ihrer stieftochter mit riesigem schwanz | xHamster", - "file_name": "Familien-unfug (teil 1)\uff1a Stiefmutter verf\u00fchrt den freund ihrer stieftochter mit riesigem schwanz [xhNTlTd].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/ed236077-0b3d-4165-a157-0a215ca85663.mp4" - }, - { - "id": "ed3df055-47bb-417b-988f-82d2e7c51f4a", - "created_date": "2024-12-29 23:53:27.937612", - "last_modified_date": "2024-12-29 23:53:27.937612", - "version": 0, - "url": "https://ge.xhamster.com/videos/family-road-trip-xhvC9bX", - "review": 0, - "should_download": 0, - "title": "Familien-Roadtrip | xHamster", - "file_name": "Familien-Roadtrip [xhvC9bX].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/ed3df055-47bb-417b-988f-82d2e7c51f4a.mp4" - }, - { - "id": "ed5aa02b-6635-4f1b-b972-81fc8f6bcc4b", - "created_date": "2024-07-25 07:29:46.637832", - "last_modified_date": "2024-07-25 07:29:46.637832", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-village-swinging-exploration-2846875", - "review": 0, - "should_download": 0, - "title": "Das Dorf schwingt erforscht | xHamster", - "file_name": "Das Dorf schwingt erforscht [2846875].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ed5aa02b-6635-4f1b-b972-81fc8f6bcc4b.mp4" - }, - { - "id": "ed882c47-0b1f-443d-bce7-fe36fac2682f", - "created_date": "2024-07-25 07:29:48.165475", - "last_modified_date": "2024-07-25 07:29:48.165475", - "version": 0, - "url": "https://ge.xhamster.com/videos/jugendlicher-babysitter-macht-verschwitzten-gruppensex-xhscfbV", - "review": 0, - "should_download": 0, - "title": "Jugendlicher Babysitter Macht Verschwitzten Gruppensex | xHamster", - "file_name": "Jugendlicher Babysitter macht verschwitzten Gruppensex [xhscfbV].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ed882c47-0b1f-443d-bce7-fe36fac2682f.mp4" - }, - { - "id": "ed972468-a0ec-4601-9efd-bcebad854786", - "created_date": "2024-07-25 07:29:46.348627", - "last_modified_date": "2024-07-25 07:29:46.348627", - "version": 0, - "url": "https://ge.xhamster.com/videos/a-beautiful-german-girl-loves-pleasing-a-cock-outdoors-xhwCQSh", - "review": 0, - "should_download": 0, - "title": "Ein sch\u00f6nes deutsches M\u00e4dchen liebt es, einen Schwanz im Freien zu befriedigen | xHamster", - "file_name": "Ein sch\u00f6nes deutsches M\u00e4dchen liebt es, einen Schwanz im Freien zu befriedigen [xhwCQSh].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ed972468-a0ec-4601-9efd-bcebad854786.mp4" - }, - { - "id": "edc29973-ef60-44e1-bd2d-5b1dc148518d", - "created_date": "2024-07-25 07:29:45.993104", - "last_modified_date": "2024-07-25 07:29:45.993104", - "version": 0, - "url": "https://ge.xhamster.com/videos/wife-shared-at-party-10366744", - "review": 0, - "should_download": 0, - "title": "Ehefrau auf der Party geteilt | xHamster", - "file_name": "Ehefrau auf der Party geteilt [10366744].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/edc29973-ef60-44e1-bd2d-5b1dc148518d.mp4" - }, - { - "id": "edc50f6d-1a45-4db3-96ad-e1108d910e3f", - "created_date": "2024-07-25 07:29:45.170871", - "last_modified_date": "2024-07-25 07:29:45.170871", - "version": 0, - "url": "https://ge.xhamster.com/videos/sommertraume-junger-madchen-nr-3-full-movie-xhcIHl6", - "review": 0, - "should_download": 0, - "title": "Sommertr\u00e4ume junger m\u00e4dchen # 3 (kompletter film) | xHamster", - "file_name": "Sommertr\u00e4ume junger m\u00e4dchen # 3 (kompletter film) [xhcIHl6].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/edc50f6d-1a45-4db3-96ad-e1108d910e3f.mp4" - }, - { - "id": "edf758b6-05a4-43d5-996b-dce819b08fe4", - "created_date": "2024-12-29 23:53:27.895919", - "last_modified_date": "2024-12-29 23:53:27.895919", - "version": 0, - "url": "https://ge.xhamster.com/videos/sugar-lyn-beard-palm-swings-2017-9921879", - "review": 0, - "should_download": 0, - "title": "Sugar Lyn Bart - Palm Swings (2017) | xHamster", - "file_name": "Sugar Lyn Bart - Palm Swings (2017) [9921879].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/edf758b6-05a4-43d5-996b-dce819b08fe4.mp4" - }, - { - "id": "ee461aa7-9b82-45e9-9796-bc441915ae85", - "created_date": "2024-07-25 07:29:45.192387", - "last_modified_date": "2024-07-25 07:29:45.192387", - "version": 0, - "url": "https://ge.xhamster.com/videos/babysitter-comes-over-early-and-spied-on-wife-sucking-off-husband-xhPuDCm", - "review": 0, - "should_download": 0, - "title": "Babysitter kommt fr\u00fch r\u00fcber und bespitzelt Ehefrau, die Ehemann lutscht | xHamster", - "file_name": "Babysitter kommt fr\u00fch r\u00fcber und bespitzelt Ehefrau, die Ehemann lutscht [xhPuDCm].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ee461aa7-9b82-45e9-9796-bc441915ae85.mp4" - }, - { - "id": "ee4a6b31-6ce4-4e31-a717-0a8d0a06a5f7", - "created_date": "2024-07-25 07:29:46.967665", - "last_modified_date": "2024-07-25 07:29:46.967665", - "version": 0, - "url": "https://ge.xhamster.com/videos/bathtub-threesome-with-the-hot-stepmom-monique-alexander-6815912", - "review": 0, - "should_download": 0, - "title": "Badewannen-Dreier mit der hei\u00dfen Stiefmutter Monique Alexander | xHamster", - "file_name": "Badewannen-Dreier mit der hei\u00dfen Stiefmutter Monique Alexander [6815912].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ee4a6b31-6ce4-4e31-a717-0a8d0a06a5f7.mp4" - }, - { - "id": "ee843861-3e4c-4ceb-8550-db022f5e2fc7", - "created_date": "2024-07-25 07:29:46.598576", - "last_modified_date": "2024-07-25 07:29:46.598576", - "version": 0, - "url": "https://ge.xhamster.com/videos/a-sibel-kekilli-classic-vol-4-13437069", - "review": 0, - "should_download": 0, - "title": "Hotel Fickmichgut | xHamster", - "file_name": "Hotel Fickmichgut [13437069].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ee843861-3e4c-4ceb-8550-db022f5e2fc7.mp4" - }, - { - "id": "ee8f2b8a-79fc-49b6-a35b-b618a01c983c", - "created_date": "2024-07-25 07:29:44.890163", - "last_modified_date": "2024-07-25 07:29:44.890163", - "version": 0, - "url": "https://ge.xhamster.com/videos/lost-bet-now-she-had-to-fuck-3-guys-at-the-same-time-xhOkSrk", - "review": 0, - "should_download": 0, - "title": "Lost Bet, jetzt musste sie 3 Typen gleichzeitig ficken | xHamster", - "file_name": "Lost Bet, jetzt musste sie 3 Typen gleichzeitig ficken [xhOkSrk].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/ee8f2b8a-79fc-49b6-a35b-b618a01c983c.mp4" - }, - { - "id": "ee92b520-3dad-4703-a2f3-8f8f1f6fe4d1", - "created_date": "2024-07-25 07:29:46.741072", - "last_modified_date": "2024-07-25 07:29:46.741072", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-neighbor-catches-me-masturbating-in-the-pool-and-invites-me-to-fuck-andrea-pardo-xhecMDu", - "review": 0, - "should_download": 0, - "title": "Meine Nachbarin erwischt mich beim Masturbieren im Pool und l\u00e4dt mich zum Ficken ein - Andrea Pardo | xHamster", - "file_name": "Meine Nachbarin erwischt mich beim Masturbieren im Pool und l\u00e4dt mich zum Ficken ein - Andrea Pardo [xhecMDu].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ee92b520-3dad-4703-a2f3-8f8f1f6fe4d1.mp4" - }, - { - "id": "ee9b512a-348e-4301-b9cd-312ed37c69d4", - "created_date": "2024-07-25 07:29:46.285885", - "last_modified_date": "2024-07-25 07:29:46.285885", - "version": 0, - "url": "https://ge.xhamster.com/videos/mature-wife-catches-husband-jerking-off-and-doesnt-mind-xho8Vlh", - "review": 0, - "should_download": 0, - "title": "Reife ehefrau erwischt ehemann beim wichsen und hat nichts dagegen | xHamster", - "file_name": "Reife ehefrau erwischt ehemann beim wichsen und hat nichts dagegen [xho8Vlh].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ee9b512a-348e-4301-b9cd-312ed37c69d4.mp4" - }, - { - "id": "eea33597-c1c4-4ddb-91c4-52ba2d9dba2f", - "created_date": "2024-07-25 07:29:46.424414", - "last_modified_date": "2024-07-25 07:29:46.424414", - "version": 0, - "url": "https://ge.xhamster.com/videos/that-was-really-hot-i-was-wondering-if-you-could-fuck-me-xhuyqu3", - "review": 0, - "should_download": 0, - "title": "Das war wirklich hei\u00df, ich habe mich gefragt, ob du mich ficken kannst | xHamster", - "file_name": "Das war wirklich hei\u00df, ich habe mich gefragt, ob du mich ficken kannst [xhuyqu3].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/eea33597-c1c4-4ddb-91c4-52ba2d9dba2f.mp4" - }, - { - "id": "eef22f5f-f3c1-4971-b63f-759316926902", - "created_date": "2024-07-25 07:29:47.517861", - "last_modified_date": "2025-01-03 11:56:51.864000", - "version": 1, - "url": "https://ge.xhamster.com/videos/insane-group-fucking-and-pissing-by-summersinners-xhG951b", - "review": 0, - "should_download": 0, - "title": "Wahnsinnige gruppe Ficken und Pissen von SummerSinners | xHamster", - "file_name": "Wahnsinnige gruppe Ficken und Pissen von SummerSinners [xhG951b].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/eef22f5f-f3c1-4971-b63f-759316926902.mp4" - }, - { - "id": "ef004f13-26bf-4729-8de6-66a30f0c2259", - "created_date": "2024-07-25 07:29:45.883816", - "last_modified_date": "2024-07-25 07:29:45.883816", - "version": 0, - "url": "https://ge.xhamster.com/videos/its-definitely-bigger-than-mypervyfamily-xhkScYv", - "review": 0, - "should_download": 0, - "title": "\"Es ist auf jeden Fall gr\u00f6\u00dfer als...\" - Mypervyfamily | xHamster", - "file_name": "\uff02Es ist auf jeden Fall gr\u00f6\u00dfer als...\uff02 - Mypervyfamily [xhkScYv].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ef004f13-26bf-4729-8de6-66a30f0c2259.mp4" - }, - { - "id": "ef83d783-8f0f-4d0b-ad50-68d98e3ec0a8", - "created_date": "2024-07-25 07:29:47.899891", - "last_modified_date": "2024-07-25 07:29:47.899891", - "version": 0, - "url": "https://ge.xhamster.com/videos/two-lucky-dudes-breach-all-the-tight-hot-young-holes-on-this-perfect-blonde-xhFEnIh", - "review": 0, - "should_download": 0, - "title": "Zwei gl\u00fcckliche Typen durchbrechen alle engen hei\u00dfen jungen L\u00f6cher dieser perfekten Blondine | xHamster", - "file_name": "Zwei gl\u00fcckliche Typen durchbrechen alle engen hei\u00dfen jungen L\u00f6cher dieser perfekten Blondine [xhFEnIh].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ef83d783-8f0f-4d0b-ad50-68d98e3ec0a8.mp4" - }, - { - "id": "efa08b76-c334-48da-b7e6-b11a4e230d37", - "created_date": "2024-07-25 07:29:44.736600", - "last_modified_date": "2024-07-25 07:29:44.736600", - "version": 0, - "url": "https://ge.xhamster.com/videos/hot-stepmom-sharon-white-shares-boyfriend-s-cock-xhqk2HR", - "review": 0, - "should_download": 0, - "title": "Hei\u00dfe Stiefmutter Sharon White teilt den Schwanz ihres Freundes | xHamster", - "file_name": "Hei\u00dfe Stiefmutter Sharon White teilt den Schwanz ihres Freundes [xhqk2HR].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/efa08b76-c334-48da-b7e6-b11a4e230d37.mp4" - }, - { - "id": "f048051c-ecc8-475a-94bc-99dcbee3abb8", - "created_date": "2024-07-25 07:29:47.999304", - "last_modified_date": "2024-07-25 07:29:47.999304", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsis-catches-me-my-friends-sniffing-her-panties-14250952", - "review": 0, - "should_download": 0, - "title": "Stiefschwester erwischt mich & meine Freunde beim Schn\u00fcffeln an ihrem H\u00f6schen | xHamster", - "file_name": "Stiefschwester erwischt mich & meine Freunde beim Schn\u00fcffeln an ihrem H\u00f6schen [14250952].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f048051c-ecc8-475a-94bc-99dcbee3abb8.mp4" - }, - { - "id": "f082dc83-be35-43c2-b6a2-ba13b72baa94", - "created_date": "2024-07-25 07:29:45.139276", - "last_modified_date": "2024-07-25 07:29:45.139276", - "version": 0, - "url": "https://ge.xhamster.com/videos/donau-sex-full-german-movie-xhRzz9S", - "review": 0, - "should_download": 0, - "title": "Donau Sex, kompletter deutscher Film | xHamster", - "file_name": "Donau Sex, kompletter deutscher Film [xhRzz9S].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/f082dc83-be35-43c2-b6a2-ba13b72baa94.mp4" - }, - { - "id": "f0b35027-8b8c-4dec-a6d7-965448eae99d", - "created_date": "2024-07-25 07:29:46.769168", - "last_modified_date": "2024-07-25 07:29:46.769168", - "version": 0, - "url": "https://ge.xhamster.com/videos/sex-sex-and-fun-6753859", - "review": 0, - "should_download": 0, - "title": "Sex Sex Sex und Spa\u00df | xHamster", - "file_name": "Sex Sex Sex und Spa\u00df [6753859].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f0b35027-8b8c-4dec-a6d7-965448eae99d.mp4" - }, - { - "id": "f0bb4c23-6b43-477c-9afe-8017ee5828b8", - "created_date": "2024-07-25 07:29:45.321819", - "last_modified_date": "2024-07-25 07:29:45.321819", - "version": 0, - "url": "https://ge.xhamster.com/videos/sarah-and-friends-11-1994-german-full-dvd-rip-xhn7u6y", - "review": 0, - "should_download": 0, - "title": "Sarah und Freunde 11 (1994, deutsch, kompletter DVD-Rip) | xHamster", - "file_name": "Sarah und Freunde 11 (1994, deutsch, kompletter DVD-Rip) [xhn7u6y].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/f0bb4c23-6b43-477c-9afe-8017ee5828b8.mp4" - }, - { - "id": "f0d9b89c-7a08-43c3-805b-b6700f14667f", - "created_date": "2024-07-25 07:29:47.487827", - "last_modified_date": "2024-07-25 07:29:47.487827", - "version": 0, - "url": "https://ge.xhamster.com/videos/german-schwanzgeile-jungfrauen-1290630", - "review": 0, - "should_download": 0, - "title": "Deutsche schwanzgeile Jungfrauen | xHamster", - "file_name": "Deutsche schwanzgeile Jungfrauen [1290630].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f0d9b89c-7a08-43c3-805b-b6700f14667f.mp4" - }, - { - "id": "f0e2ad5e-46e2-490b-bdb1-f7366d6190b4", - "created_date": "2024-07-25 07:29:47.430626", - "last_modified_date": "2024-07-25 07:29:47.430626", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepbrother-stepsister-learn-to-get-along-family-therapy-xhsyAAg", - "review": 0, - "should_download": 0, - "title": "Stiefbruder und Stiefschwester lernen, miteinander auszukommen - Familientherapie | xHamster", - "file_name": "Stiefbruder und Stiefschwester lernen, miteinander auszukommen - Familientherapie [xhsyAAg].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f0e2ad5e-46e2-490b-bdb1-f7366d6190b4.mp4" - }, - { - "id": "f0f9a018-3e95-461c-aa52-3527e69f903d", - "created_date": "2024-07-25 07:29:47.813306", - "last_modified_date": "2024-07-25 07:29:47.813306", - "version": 0, - "url": "https://ge.xhamster.com/videos/lust-boat-1984-9976817", - "review": 0, - "should_download": 0, - "title": "Lust Boat (1984) | xHamster", - "file_name": "Lust Boat (1984) [9976817].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f0f9a018-3e95-461c-aa52-3527e69f903d.mp4" - }, - { - "id": "f103a45a-754d-4a73-8a5f-0335d1f71d22", - "created_date": "2024-07-25 07:29:47.563713", - "last_modified_date": "2024-07-25 07:29:47.563713", - "version": 0, - "url": "https://ge.xhamster.com/videos/die-wilden-lueste-meiner-schulfreundinnen-1984-6220701", - "review": 0, - "should_download": 0, - "title": "Die Wilden Lueste Meiner Schulfreundinnen 1984: Porn db | xHamster", - "file_name": "Die wilden Lueste meiner Schulfreundinnen (1984) [6220701].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f103a45a-754d-4a73-8a5f-0335d1f71d22.mp4" - }, - { - "id": "f11b051f-96a4-4b75-9e12-ab703e7c5c90", - "created_date": "2024-07-25 07:29:47.608383", - "last_modified_date": "2024-07-25 07:29:47.608383", - "version": 0, - "url": "https://ge.xhamster.com/videos/schulmadchen-report-2-1971-5765359", - "review": 0, - "should_download": 0, - "title": "Schulmadchen-Bericht 2 (1971) | xHamster", - "file_name": "Schulmadchen-Bericht 2 (1971) [5765359].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f11b051f-96a4-4b75-9e12-ab703e7c5c90.mp4" - }, - { - "id": "f11b504d-5742-4bc1-831f-15562ec4c451", - "created_date": "2024-07-25 07:29:44.733091", - "last_modified_date": "2024-07-25 07:29:44.733091", - "version": 0, - "url": "https://ge.xhamster.com/videos/horny-friends-fucking-by-the-pool-xhMYg9o", - "review": 0, - "should_download": 0, - "title": "Geile Freunde ficken am Pool | xHamster", - "file_name": "Geile Freunde ficken am Pool [xhMYg9o].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f11b504d-5742-4bc1-831f-15562ec4c451.mp4" - }, - { - "id": "f193b97e-ef9e-465e-842c-628b4911868c", - "created_date": "2024-07-25 07:29:47.832210", - "last_modified_date": "2024-07-25 07:29:47.832210", - "version": 0, - "url": "https://ge.xhamster.com/videos/double-squirt-vol-04-xhPqT4A", - "review": 0, - "should_download": 0, - "title": "Doppelter Squirt !!! - vol. # 04 | xHamster", - "file_name": "Doppelter Squirt !!! - vol. # 04 [xhPqT4A].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f193b97e-ef9e-465e-842c-628b4911868c.mp4" - }, - { - "id": "f2094653-7b0f-4a50-9cf1-f3625aefcd0b", - "created_date": "2024-07-25 07:29:45.505698", - "last_modified_date": "2024-07-25 07:29:45.505698", - "version": 0, - "url": "https://ge.xhamster.com/videos/fuck-at-the-cinema-watching-a-porn-movie-xhxOoSG", - "review": 0, - "should_download": 0, - "title": "Fick im kino, w\u00e4hrend du einen pornofilm gucke | xHamster", - "file_name": "Fick im kino, w\u00e4hrend du einen pornofilm gucke [xhxOoSG].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f2094653-7b0f-4a50-9cf1-f3625aefcd0b.mp4" - }, - { - "id": "f277e107-8428-4ad3-bdfc-0ee35c1581f6", - "created_date": "2024-12-29 23:53:27.890471", - "last_modified_date": "2024-12-29 23:53:27.890471", - "version": 0, - "url": "https://ge.xhamster.com/videos/young-teacher-lets-students-fuck-at-her-place-7509586", - "review": 0, - "should_download": 0, - "title": "Junge Lehrerin l\u00e4sst Sch\u00fcler bei ihr ficken | xHamster", - "file_name": "Junge Lehrerin l\u00e4sst Sch\u00fcler bei ihr ficken [7509586].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/f277e107-8428-4ad3-bdfc-0ee35c1581f6.mp4" - }, - { - "id": "f2a439d3-aeaf-44a9-aee2-89b950ed21c6", - "created_date": "2024-07-25 07:29:44.650780", - "last_modified_date": "2024-07-25 07:29:44.650780", - "version": 0, - "url": "https://ge.xhamster.com/videos/mom-daughter-and-step-son-have-hardcore-sex-xhfhVdk", - "review": 0, - "should_download": 0, - "title": "Mutter, Stieftochter und Stiefsohn haben Hardcore-Sex | xHamster", - "file_name": "Mutter, Stieftochter und Stiefsohn haben Hardcore-Sex [xhfhVdk].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f2a439d3-aeaf-44a9-aee2-89b950ed21c6.mp4" - }, - { - "id": "f2e6aa40-38c9-4d15-88a1-786a32dd4adc", - "created_date": "2024-07-25 07:29:46.532161", - "last_modified_date": "2024-07-25 07:29:46.532161", - "version": 0, - "url": "https://ge.xhamster.com/videos/private-anny-aurora-and-alexis-crystal-celebrate-with-an-orgy-8793899", - "review": 0, - "should_download": 0, - "title": "Anny Aurora und Alexis Crystal feiern mit einer Orgie | xHamster", - "file_name": "Anny Aurora und Alexis Crystal feiern mit einer Orgie [8793899].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f2e6aa40-38c9-4d15-88a1-786a32dd4adc.mp4" - }, - { - "id": "f31c48cf-5cc8-4ee5-b284-0092b73657c6", - "created_date": "2024-07-25 07:29:47.679675", - "last_modified_date": "2024-07-25 07:29:47.679675", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsis-says-i-dont-care-i-just-want-you-to-be-in-me-xhz4ErN", - "review": 0, - "should_download": 0, - "title": "Stiefschwester sagt - es ist mir egal, ich will nur, dass du in mir bist | xHamster", - "file_name": "Stiefschwester sagt - es ist mir egal, ich will nur, dass du in mir bist [xhz4ErN].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f31c48cf-5cc8-4ee5-b284-0092b73657c6.mp4" - }, - { - "id": "f3793acd-e656-4c80-af82-4016013a83e1", - "created_date": "2024-10-07 20:47:56.415185", - "last_modified_date": "2024-10-21 16:33:50.480000", - "version": 1, - "url": "https://ge.xhamster.com/videos/vintage-70s-danish-sex-mad-maids-german-dub-cc79-212210", - "review": 0, - "should_download": 0, - "title": "Retro 70er d\u00e4nisch - sex-verr\u00fcckte Zimmerm\u00e4dchen (deutsche Synchronisation) - cc79 | xHamster", - "file_name": "Retro 70er d\u00e4nisch - sex-verr\u00fcckte Zimmerm\u00e4dchen (deutsche Synchronisation) - cc79 [212210].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/f3793acd-e656-4c80-af82-4016013a83e1.mp4" - }, - { - "id": "f382aed3-d899-41c0-8cac-8eb11f1bcba9", - "created_date": "2024-08-28 23:21:54.352448", - "last_modified_date": "2024-08-28 23:21:54.352448", - "version": 0, - "url": "https://ge.xhamster.com/videos/ultimate-home-orgy-5-1-dag83-9038173", - "review": 0, - "should_download": 0, - "title": "Ultimative Heimorgie 5.1 - dag83 | xHamster", - "file_name": "Ultimative Heimorgie 5.1 - dag83 [9038173].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/f382aed3-d899-41c0-8cac-8eb11f1bcba9.mp4" - }, - { - "id": "f3a734d0-c7e8-4491-8a7f-fafc9596545b", - "created_date": "2024-07-25 07:29:44.579063", - "last_modified_date": "2024-07-25 07:29:44.579063", - "version": 0, - "url": "https://ge.xhamster.com/videos/remembering-the-hot-days-of-summer-xhTh0ec", - "review": 0, - "should_download": 0, - "title": "Sich an die hei\u00dfen Sommertage erinnern | xHamster", - "file_name": "Sich an die hei\u00dfen Sommertage erinnern [xhTh0ec].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f3a734d0-c7e8-4491-8a7f-fafc9596545b.mp4" - }, - { - "id": "f43b0f09-5761-416b-b72d-fcb3744a0187", - "created_date": "2024-07-25 07:29:47.835862", - "last_modified_date": "2024-07-25 07:29:47.835862", - "version": 0, - "url": "https://ge.xhamster.com/videos/group-of-slutty-teens-get-smashed-by-jmac-on-spring-break-6168186", - "review": 0, - "should_download": 0, - "title": "Eine Gruppe versauter Teenager wird im Spring Break von Jmac zertr\u00fcmmert | xHamster", - "file_name": "Eine Gruppe versauter Teenager wird im Spring Break von Jmac zertr\u00fcmmert [6168186].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f43b0f09-5761-416b-b72d-fcb3744a0187.mp4" - }, - { - "id": "f490aea8-3051-4eb6-bc66-5e7c9f83df12", - "created_date": "2024-07-25 07:29:46.787750", - "last_modified_date": "2024-07-25 07:29:46.787750", - "version": 0, - "url": "https://ge.xhamster.com/videos/nothing-can-stop-hottie-angel-youngs-and-xander-from-getting-fucked-not-even-the-heavy-rain-brazzers-xhLXHLh", - "review": 0, - "should_download": 0, - "title": "Nichts kann verhindern, dass hottie angel youngs und xander davon abhalten, gefickt zu werden, nicht einmal der schwere Regen - BRAZZERS | xHamster", - "file_name": "Nichts kann verhindern, dass hottie angel youngs und xander davon abhalten, gefickt zu werden, nicht einmal der schwere Regen - BRAZZERS [xhLXHLh].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f490aea8-3051-4eb6-bc66-5e7c9f83df12.mp4" - }, - { - "id": "f4bf9363-e8bc-4541-9cb6-12caba03fcd0", - "created_date": "2024-07-25 07:29:44.853500", - "last_modified_date": "2024-07-25 07:29:44.853500", - "version": 0, - "url": "https://ge.xhamster.com/videos/my-stepmother-and-my-stepsister-are-deep-throat-fucked-by-four-of-my-best-friends-xhhWhhJ", - "review": 0, - "should_download": 0, - "title": "Meine stiefmutter und meine stiefschwester werden von vier meiner besten freunde tief in den hals gefickt! | xHamster", - "file_name": "Meine stiefmutter und meine stiefschwester werden von vier meiner besten freunde tief in den hals gefickt! [xhhWhhJ].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/f4bf9363-e8bc-4541-9cb6-12caba03fcd0.mp4" - }, - { - "id": "f4ff15aa-47d0-493a-8048-3da1a986aa1c", - "created_date": "2024-07-25 07:29:47.351700", - "last_modified_date": "2024-07-25 07:29:47.351700", - "version": 0, - "url": "https://ge.xhamster.com/videos/soapy-stepsisters-part-1-xhn84pZ", - "review": 0, - "should_download": 0, - "title": "Seifige Stiefschwestern Teil 1 | xHamster", - "file_name": "Seifige Stiefschwestern Teil 1 [xhn84pZ].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f4ff15aa-47d0-493a-8048-3da1a986aa1c.mp4" - }, - { - "id": "f52623c9-6e17-4eb4-80da-e1ca2b62582b", - "created_date": "2024-07-25 07:29:46.691414", - "last_modified_date": "2024-07-25 07:29:46.691414", - "version": 0, - "url": "https://ge.xhamster.com/videos/a-model-family-full-movie-xh2xAvX", - "review": 0, - "should_download": 0, - "title": "Eine Modellfamilie! (kompletter Film) | xHamster", - "file_name": "Eine Modellfamilie! (kompletter Film) [xh2xAvX].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f52623c9-6e17-4eb4-80da-e1ca2b62582b.mp4" - }, - { - "id": "f572c079-c845-4112-8bd2-fb0dd0629e75", - "created_date": "2024-07-25 07:29:44.921631", - "last_modified_date": "2024-07-25 07:29:44.921631", - "version": 0, - "url": "https://ge.xhamster.com/videos/desideria-episode-3-xh1HfiY", - "review": 0, - "should_download": 0, - "title": "Desideria - Episode 3, Free Romantic HD Porn 04 | xHamster", - "file_name": "Desideria - Episode 3 [xh1HfiY].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f572c079-c845-4112-8bd2-fb0dd0629e75.mp4" - }, - { - "id": "f580ebd8-b1da-45a8-ac41-773708bf2a7e", - "created_date": "2024-11-10 16:53:33.483206", - "last_modified_date": "2024-11-10 16:53:33.483206", - "version": 0, - "url": "https://ge.xhamster.com/videos/caligola-la-storia-mai-raccontata-hd-10028296", - "review": 0, - "should_download": 0, - "title": "Caligola - La Storia Mai Raccontata HD, Porn fd | xHamster", - "file_name": "Caligola - La Storia Mai Raccontata (HD) [10028296].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/f580ebd8-b1da-45a8-ac41-773708bf2a7e.mp4" - }, - { - "id": "f5878d71-7ea5-4df5-adff-487194279b93", - "created_date": "2024-10-21 15:08:43.548046", - "last_modified_date": "2024-10-21 16:33:54.964000", - "version": 1, - "url": "https://ge.xhamster.com/videos/gangbang-in-willi-s-bar-xhI0ys2", - "review": 0, - "should_download": 0, - "title": "Gangbang in Willis Bar | xHamster", - "file_name": "Gangbang in Willis Bar [xhI0ys2].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/f5878d71-7ea5-4df5-adff-487194279b93.mp4" - }, - { - "id": "f5cb4ee1-bbd2-49e8-9d9f-c1ac3b48ebf2", - "created_date": "2024-07-25 07:29:47.355166", - "last_modified_date": "2024-07-25 07:29:47.355166", - "version": 0, - "url": "https://ge.xhamster.com/videos/first-time-double-vaginal-take-it-easy-on-guys-5956121", - "review": 0, - "should_download": 0, - "title": "Zum ersten Mal doppelt vaginal, nimm es einfach mit Jungs | xHamster", - "file_name": "Zum ersten Mal doppelt vaginal, nimm es einfach mit Jungs [5956121].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f5cb4ee1-bbd2-49e8-9d9f-c1ac3b48ebf2.mp4" - }, - { - "id": "f5d60aac-d5dc-4d93-893a-b66f7c6cde79", - "created_date": "2024-07-25 07:29:47.032147", - "last_modified_date": "2024-07-25 07:29:47.032147", - "version": 0, - "url": "https://ge.xhamster.com/videos/nice-beach-holyday-9620808", - "review": 0, - "should_download": 0, - "title": "Sch\u00f6ner Strandurlaub | xHamster", - "file_name": "Sch\u00f6ner Strandurlaub [9620808].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f5d60aac-d5dc-4d93-893a-b66f7c6cde79.mp4" - }, - { - "id": "f62d4a5f-cbf7-45a2-b8a7-81bad7731ddb", - "created_date": "2024-07-25 07:29:45.363987", - "last_modified_date": "2024-07-25 07:29:45.363987", - "version": 0, - "url": "https://ge.xhamster.com/videos/busty-ginger-stepdaughter-with-braces-needs-cum-on-her-tits-13991028", - "review": 0, - "should_download": 0, - "title": "Vollbusige rothaarige Stieftochter mit Zahnspange braucht Sperma auf ihre Titten | xHamster", - "file_name": "Vollbusige rothaarige Stieftochter mit Zahnspange braucht Sperma auf ihre Titten [13991028].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f62d4a5f-cbf7-45a2-b8a7-81bad7731ddb.mp4" - }, - { - "id": "f63f1ad1-bc42-4fa0-ab06-65bfa1a9cbd6", - "created_date": "2024-07-25 07:29:48.006530", - "last_modified_date": "2024-07-25 07:29:48.006530", - "version": 0, - "url": "https://ge.xhamster.com/videos/wife-explores-sex-of-all-kinds-10739198", - "review": 0, - "should_download": 0, - "title": "Ehefrau erforscht Sex aller Art | xHamster", - "file_name": "Ehefrau erforscht Sex aller Art [10739198].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f63f1ad1-bc42-4fa0-ab06-65bfa1a9cbd6.mp4" - }, - { - "id": "f6496ffb-dca6-45cb-96a2-24198ea347c5", - "created_date": "2024-07-25 07:29:45.563668", - "last_modified_date": "2024-07-25 07:29:45.563668", - "version": 0, - "url": "https://ge.xhamster.com/videos/mary-jane-1972-us-full-movie-rhonda-king-dvd-rip-xh6AWpx", - "review": 0, - "should_download": 0, - "title": "Mary Jane (1972, US, kompletter Film, Rhonda King, DVD Rip) | xHamster", - "file_name": "Mary Jane (1972, US, kompletter Film, Rhonda King, DVD Rip) [xh6AWpx].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/f6496ffb-dca6-45cb-96a2-24198ea347c5.mp4" - }, - { - "id": "f657f146-e3e7-45b7-89a9-af1c049e135c", - "created_date": "2024-07-25 07:29:45.442335", - "last_modified_date": "2024-07-25 07:29:45.442335", - "version": 0, - "url": "https://ge.xhamster.com/videos/hello-wild-90s-we-miss-you-8842932", - "review": 0, - "should_download": 0, - "title": "Hallo, wilde 90er, wir vermissen dich! | xHamster", - "file_name": "Hallo, wilde 90er, wir vermissen dich! [8842932].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f657f146-e3e7-45b7-89a9-af1c049e135c.mp4" - }, - { - "id": "f667b81c-6202-4711-ac5d-d755b9179a04", - "created_date": "2024-07-25 07:29:45.540029", - "last_modified_date": "2024-07-25 07:29:45.540029", - "version": 0, - "url": "https://ge.xhamster.com/videos/husband-and-his-friend-fuck-wife-on-hike-in-the-desert-giving-her-a-double-creampie-xhlhBvy", - "review": 0, - "should_download": 0, - "title": "Ehemann und sein Freund ficken Ehefrau auf Wanderung in der W\u00fcste und geben ihr einen doppelten Creampie | xHamster", - "file_name": "Ehemann und sein Freund ficken Ehefrau auf Wanderung in der W\u00fcste und geben ihr einen doppelten Creampie [xhlhBvy].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/f667b81c-6202-4711-ac5d-d755b9179a04.mp4" - }, - { - "id": "f68d0b56-59d8-43d3-9aaa-72f2ced1dd3d", - "created_date": "2024-12-29 23:53:27.950469", - "last_modified_date": "2024-12-29 23:53:27.950469", - "version": 0, - "url": "https://ge.xhamster.com/videos/sister-porn-14231224", - "review": 0, - "should_download": 0, - "title": "Schwester-Porno | xHamster", - "file_name": "Schwester-Porno [14231224].mp4", - "path": null, - "cloud_link": "/media/tpeetz/media1/f68d0b56-59d8-43d3-9aaa-72f2ced1dd3d.mp4" - }, - { - "id": "f696911d-718d-4b0b-bcbc-55fe334fa457", - "created_date": "2024-07-25 07:29:46.849943", - "last_modified_date": "2024-07-25 07:29:46.849943", - "version": 0, - "url": "https://ge.xhamster.com/videos/lets-get-nasty-full-original-movie-xhW6wcp", - "review": 0, - "should_download": 0, - "title": "Lass uns b\u00f6se werden (kompletter Originalfilm) | xHamster", - "file_name": "Lass uns b\u00f6se werden (kompletter Originalfilm) [xhW6wcp].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f696911d-718d-4b0b-bcbc-55fe334fa457.mp4" - }, - { - "id": "f698823e-7494-4f7a-a030-6b4a85fb5e84", - "created_date": "2024-11-01 21:08:26.929393", - "last_modified_date": "2024-11-01 21:08:26.929393", - "version": 0, - "url": "https://ge.xhamster.com/videos/school-girl-fucks-two-guys-after-school-10311343", - "review": 0, - "should_download": 0, - "title": "Schulm\u00e4dchen fickt zwei Typen nach der Schule | xHamster", - "file_name": "Schulm\u00e4dchen fickt zwei Typen nach der Schule [10311343].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/f698823e-7494-4f7a-a030-6b4a85fb5e84.mp4" - }, - { - "id": "f6a5a528-6a0a-4854-9840-7892177eae90", - "created_date": "2024-07-25 07:29:46.098955", - "last_modified_date": "2024-07-25 07:29:46.098955", - "version": 0, - "url": "https://ge.xhamster.com/videos/familystrokes-hot-stepmom-teaches-her-teens-how-to-fuck-12705727", - "review": 0, - "should_download": 0, - "title": "Familystrokes - eine hei\u00dfe Stiefmutter bringt ihrem Teenager bei, wie man fickt | xHamster", - "file_name": "Familystrokes - eine hei\u00dfe Stiefmutter bringt ihrem Teenager bei, wie man fickt [12705727].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f6a5a528-6a0a-4854-9840-7892177eae90.mp4" - }, - { - "id": "f6ea70f6-4099-4176-aa68-15a24dcedccf", - "created_date": "2024-10-07 20:47:56.422794", - "last_modified_date": "2024-10-21 16:34:00.223000", - "version": 1, - "url": "https://ge.xhamster.com/videos/teen-blonde-caught-cheating-and-fucked-in-the-ass-xhFkr1m", - "review": 0, - "should_download": 0, - "title": "Teenie-blondine erwischt beim betr\u00fcgen und wurde in den arsch gefickt | xHamster", - "file_name": "Teenie-blondine erwischt beim betr\u00fcgen und wurde in den arsch gefickt [xhFkr1m].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/f6ea70f6-4099-4176-aa68-15a24dcedccf.mp4" - }, - { - "id": "f70776d5-2863-4a6c-a952-61c567648dd4", - "created_date": "2024-07-25 07:29:47.422280", - "last_modified_date": "2025-01-03 11:56:56.069000", - "version": 1, - "url": "https://ge.xhamster.com/videos/wine-and-group-sex-party-1075537", - "review": 0, - "should_download": 0, - "title": "Wein- und Gruppensex-Party | xHamster", - "file_name": "Wein- und Gruppensex-Party [1075537].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f70776d5-2863-4a6c-a952-61c567648dd4.mp4" - }, - { - "id": "f7133fd5-f05b-4c9c-ab74-308a683227f8", - "created_date": "2024-07-25 07:29:45.336750", - "last_modified_date": "2024-07-25 07:29:45.336750", - "version": 0, - "url": "https://ge.xhamster.com/videos/thirsty-babes-for-cock-looking-on-the-road-for-some-adventure-xhR0S3U", - "review": 0, - "should_download": 0, - "title": "Durstige Sch\u00e4tzchen f\u00fcr Schwanz suchen auf der Stra\u00dfe nach einem Abenteuer | xHamster", - "file_name": "Durstige Sch\u00e4tzchen f\u00fcr Schwanz suchen auf der Stra\u00dfe nach einem Abenteuer [xhR0S3U].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f7133fd5-f05b-4c9c-ab74-308a683227f8.mp4" - }, - { - "id": "f75b7fe6-ad33-4d62-9c15-fac12d73c041", - "created_date": "2024-07-25 07:29:47.435142", - "last_modified_date": "2024-07-25 07:29:47.435142", - "version": 0, - "url": "https://ge.xhamster.com/videos/2-guys-caught-german-girl-in-public-and-picked-her-up-for-a-threesome-mmf-xhblDyR", - "review": 0, - "should_download": 0, - "title": "2 M\u00e4nner erwischen deutsche teen am See und \u00fcberreden sie zum dreier | xHamster", - "file_name": "2 M\u00e4nner erwischen deutsche teen am See und \u00fcberreden sie zum dreier [xhblDyR].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f75b7fe6-ad33-4d62-9c15-fac12d73c041.mp4" - }, - { - "id": "f79b0cbe-4ad7-415c-86d0-658d2ade0ed6", - "created_date": "2024-07-25 07:29:47.646537", - "last_modified_date": "2024-07-25 07:29:47.646537", - "version": 0, - "url": "https://ge.xhamster.com/videos/help-me-suck-your-stepbros-cock-12723528", - "review": 0, - "should_download": 0, - "title": "Hilf mir, den Schwanz deines Stiefbruders zu lutschen | xHamster", - "file_name": "Hilf mir, den Schwanz deines Stiefbruders zu lutschen [12723528].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f79b0cbe-4ad7-415c-86d0-658d2ade0ed6.mp4" - }, - { - "id": "f7e2a208-3323-415e-8e28-b2ab9a002128", - "created_date": "2024-07-25 07:29:45.135471", - "last_modified_date": "2024-07-25 07:29:45.135471", - "version": 0, - "url": "https://ge.xhamster.com/videos/klassengeile-1977-6317304", - "review": 0, - "should_download": 0, - "title": "Klassengeile 1977: Free Orgy Porn Video 2e | xHamster", - "file_name": "Klassengeile (1977) [6317304].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f7e2a208-3323-415e-8e28-b2ab9a002128.mp4" - }, - { - "id": "f805dcca-513b-42f0-b516-84650a3536cd", - "created_date": "2024-07-25 07:29:46.166274", - "last_modified_date": "2024-07-25 07:29:46.166274", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-boss-is-fucking-the-secretary-xhIvgCm", - "review": 0, - "should_download": 0, - "title": "Der Chef fickt die Sekret\u00e4rin | xHamster", - "file_name": "Der Chef fickt die Sekret\u00e4rin [xhIvgCm].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f805dcca-513b-42f0-b516-84650a3536cd.mp4" - }, - { - "id": "f8351b1b-b554-4d24-851e-a54e5e89eadb", - "created_date": "2024-07-25 07:29:45.382600", - "last_modified_date": "2024-07-25 07:29:45.382600", - "version": 0, - "url": "https://ge.xhamster.com/videos/nervous-couple-end-up-double-teaming-busty-masseuse-xhJ8Fbx", - "review": 0, - "should_download": 0, - "title": "Nerv\u00f6se Paare landen im Doppel, vollbusige Masseuse | xHamster", - "file_name": "Nerv\u00f6se Paare landen im Doppel, vollbusige Masseuse [xhJ8Fbx].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f8351b1b-b554-4d24-851e-a54e5e89eadb.mp4" - }, - { - "id": "f844a849-973b-4452-8402-73e2121ed015", - "created_date": "2024-07-25 07:29:46.641562", - "last_modified_date": "2024-07-25 07:29:46.641562", - "version": 0, - "url": "https://ge.xhamster.com/videos/momsteachsex-my-bf-caught-punished-by-step-mom-6876583", - "review": 0, - "should_download": 0, - "title": "Momsteachsex - mein Freund von Stiefmutter erwischt und bestraft | xHamster", - "file_name": "Momsteachsex - mein Freund von Stiefmutter erwischt und bestraft [6876583].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f844a849-973b-4452-8402-73e2121ed015.mp4" - }, - { - "id": "f897067a-c74c-4bf5-960e-926d78a74ef0", - "created_date": "2024-07-25 07:29:46.733090", - "last_modified_date": "2025-01-03 14:38:52.127000", - "version": 2, - "url": "https://ge.xhamster.com/videos/stiev-sohn-hynotiersiert-und-fick-glaeubige-stievmutte-xhWYq0y", - "review": 0, - "should_download": 0, - "title": "Video wurde gel\u00f6scht", - "file_name": "stiev sohn hynotiersiert und fick Glaeubige stievmutte [xhWYq0y].mp4", - "path": "/mnt/e/media", - "cloud_link": "" - }, - { - "id": "f8e0543f-e1e1-4a65-b486-d35ec497c31c", - "created_date": "2024-07-25 07:29:46.837259", - "last_modified_date": "2024-07-25 07:29:46.837259", - "version": 0, - "url": "https://ge.xhamster.com/videos/classic-bride-comforters-230164", - "review": 0, - "should_download": 0, - "title": "Classic - Brautdecken | xHamster", - "file_name": "Classic - Brautdecken [230164].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f8e0543f-e1e1-4a65-b486-d35ec497c31c.mp4" - }, - { - "id": "f8e37b1f-1cdd-4100-9968-a2f1ca2de6cd", - "created_date": "2024-07-25 07:29:44.345173", - "last_modified_date": "2024-07-25 07:29:44.345173", - "version": 0, - "url": "https://ge.xhamster.com/videos/busty-redhead-annabel-redd-rides-roommate-s-big-cock-s14-e12-xhWwhdk", - "review": 0, - "should_download": 0, - "title": "Die vollbusige rothaarige Annabel Redd reitet den gro\u00dfen Schwanz des Mitbewohners - s14: e12 | xHamster", - "file_name": "Die vollbusige rothaarige Annabel Redd reitet den gro\u00dfen Schwanz des Mitbewohners - s14\uff1a e12 [xhWwhdk].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f8e37b1f-1cdd-4100-9968-a2f1ca2de6cd.mp4" - }, - { - "id": "f966d568-677c-4d59-91f5-8daefa272e7d", - "created_date": "2024-07-25 07:29:47.653796", - "last_modified_date": "2024-07-25 07:29:47.653796", - "version": 0, - "url": "https://ge.xhamster.com/videos/french-family-reunion-part-iv-529075", - "review": 0, - "should_download": 0, - "title": "Franz\u00f6sisches Familientreffen, Teil iv | xHamster", - "file_name": "Franz\u00f6sisches Familientreffen, Teil iv [529075].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f966d568-677c-4d59-91f5-8daefa272e7d.mp4" - }, - { - "id": "f97895b4-2e10-47c5-af1b-9d037da0adfe", - "created_date": "2024-07-25 07:29:46.885388", - "last_modified_date": "2024-07-25 07:29:46.885388", - "version": 0, - "url": "https://ge.xhamster.com/videos/student-orgy-college-girls-gone-wild-xhqBUKV", - "review": 0, - "should_download": 0, - "title": "Studentenorgie: college-m\u00e4dchen, die wild geworden sind | xHamster", - "file_name": "Studentenorgie\uff1a college-m\u00e4dchen, die wild geworden sind [xhqBUKV].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f97895b4-2e10-47c5-af1b-9d037da0adfe.mp4" - }, - { - "id": "f9ce1560-d97f-426b-8ec1-8aeb4e71333e", - "created_date": "2024-07-25 07:29:47.718551", - "last_modified_date": "2024-07-25 07:29:47.718551", - "version": 0, - "url": "https://ge.xhamster.com/videos/stepsis-kenzie-reeves-says-i-just-need-some-dick-xhy1F62", - "review": 0, - "should_download": 0, - "title": "Stiefschwester Kenzie Reeves sagt, ich brauche nur einen Schwanz! | xHamster", - "file_name": "Stiefschwester Kenzie Reeves sagt, ich brauche nur einen Schwanz! [xhy1F62].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/f9ce1560-d97f-426b-8ec1-8aeb4e71333e.mp4" - }, - { - "id": "fb0fb178-b5f0-4665-8727-c7845a202907", - "created_date": "2024-11-10 16:53:33.477460", - "last_modified_date": "2024-11-10 16:53:33.477460", - "version": 0, - "url": "https://ge.xhamster.com/videos/initiation-of-young-lady-1979-12425395", - "review": 0, - "should_download": 0, - "title": "Initiation einer jungen Dame (1979) | xHamster", - "file_name": "Initiation einer jungen Dame (1979) [12425395].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/fb0fb178-b5f0-4665-8727-c7845a202907.mp4" - }, - { - "id": "fb2f2077-777d-464c-b11d-a30e3dbdaeee", - "created_date": "2024-07-25 07:29:45.696082", - "last_modified_date": "2024-07-25 07:29:45.696082", - "version": 0, - "url": "https://ge.xhamster.com/videos/please-gangbang-my-stepsister-episode-01-xhu0fCk", - "review": 0, - "should_download": 0, - "title": "Bitte, Gangbang meiner Stiefschwester !!! - Episode # 01 | xHamster", - "file_name": "Bitte, Gangbang meiner Stiefschwester !!! - Episode # 01 [xhu0fCk].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fb2f2077-777d-464c-b11d-a30e3dbdaeee.mp4" - }, - { - "id": "fb440beb-0c50-46de-ae69-0ca47a6711ef", - "created_date": "2024-07-25 07:29:46.507343", - "last_modified_date": "2024-07-25 07:29:46.507343", - "version": 0, - "url": "https://ge.xhamster.com/videos/massive-cock-dorm-fling-threesome-in-shared-accommodation-xhIRQWT", - "review": 0, - "should_download": 0, - "title": "Massiver schwanz-schlafsaal Fling: dreier in einer wohngemeinschaft | xHamster", - "file_name": "Massiver schwanz-schlafsaal Fling\uff1a dreier in einer wohngemeinschaft [xhIRQWT].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fb440beb-0c50-46de-ae69-0ca47a6711ef.mp4" - }, - { - "id": "fb58cc85-0931-4981-8fe6-c39be21e83ca", - "created_date": "2024-08-28 23:21:54.348385", - "last_modified_date": "2024-08-28 23:21:54.348385", - "version": 0, - "url": "https://ge.xhamster.com/videos/brother-wins-the-bet-10876076", - "review": 0, - "should_download": 0, - "title": "Bruder gewinnt die Wette | xHamster", - "file_name": "Bruder gewinnt die Wette [10876076].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/fb58cc85-0931-4981-8fe6-c39be21e83ca.mp4" - }, - { - "id": "fb7f20ff-d141-4d35-956a-980abdbbf593", - "created_date": "2024-07-25 07:29:47.524845", - "last_modified_date": "2025-01-03 11:57:04.163000", - "version": 1, - "url": "https://ge.xhamster.com/videos/bad-stepmom-s20-e7-xhrubvP", - "review": 0, - "should_download": 0, - "title": "Bad Stepmom - S20 E7: Taboo HD Porn Video c2 | xHamster", - "file_name": "B\u00f6se stiefmutter - s20\uff1a e7 [xhrubvP].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fb7f20ff-d141-4d35-956a-980abdbbf593.mp4" - }, - { - "id": "fb802800-25f8-4f13-970b-bc66d87e0c7b", - "created_date": "2024-07-25 07:29:47.914687", - "last_modified_date": "2024-07-25 07:29:47.914687", - "version": 0, - "url": "https://ge.xhamster.com/videos/wild-holidays-episode-5-xhC9pvW", - "review": 0, - "should_download": 0, - "title": "Wilde Feiertage - Episode 5 | xHamster", - "file_name": "Wilde Feiertage - Episode 5 [xhC9pvW].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fb802800-25f8-4f13-970b-bc66d87e0c7b.mp4" - }, - { - "id": "fba4c507-aa26-4ea0-8b04-33d3563e0e01", - "created_date": "2024-07-25 07:29:44.636229", - "last_modified_date": "2024-07-25 07:29:44.636229", - "version": 0, - "url": "https://ge.xhamster.com/videos/familie-4216589", - "review": 0, - "should_download": 0, - "title": "Familie: Hardcore & Old & Young Porn Video 2a | xHamster", - "file_name": "Familie [4216589].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fba4c507-aa26-4ea0-8b04-33d3563e0e01.mp4" - }, - { - "id": "fbf0b604-a7f8-4a29-9375-b42af2d565a9", - "created_date": "2024-07-25 07:29:44.674205", - "last_modified_date": "2024-07-25 07:29:44.674205", - "version": 0, - "url": "https://ge.xhamster.com/videos/soaking-step-milfs-pussy-caitlin-bell-xhdJGFF", - "review": 0, - "should_download": 0, - "title": "Tr\u00e4nken in die Muschi der Stiefmutter, klingelt | xHamster", - "file_name": "Tr\u00e4nken in die Muschi der Stiefmutter, klingelt [xhdJGFF].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fbf0b604-a7f8-4a29-9375-b42af2d565a9.mp4" - }, - { - "id": "fc0f835c-c668-4f46-8fea-ffd671cbf813", - "created_date": "2024-07-25 07:29:45.467769", - "last_modified_date": "2024-07-25 07:29:45.467769", - "version": 0, - "url": "https://ge.xhamster.com/videos/snow-white-original-hd-11553372", - "review": 0, - "should_download": 0, - "title": "Schneewittchen Original hd | xHamster", - "file_name": "Schneewittchen Original hd [11553372].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fc0f835c-c668-4f46-8fea-ffd671cbf813.mp4" - }, - { - "id": "fc2a73ae-21d8-443a-b54e-491254b3706a", - "created_date": "2024-08-28 23:21:54.368688", - "last_modified_date": "2024-08-28 23:21:54.368688", - "version": 0, - "url": "https://ge.xhamster.com/videos/vintage-1960s-summer-camp-memories-4176629", - "review": 0, - "should_download": 0, - "title": "Retro - 1960er - Sommerlager-Erinnerungen | xHamster", - "file_name": "Retro - 1960er - Sommerlager-Erinnerungen [4176629].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/fc2a73ae-21d8-443a-b54e-491254b3706a.mp4" - }, - { - "id": "fc6531da-e50b-42f1-b87e-24e64634f999", - "created_date": "2024-07-25 07:29:46.539346", - "last_modified_date": "2024-07-25 07:29:46.539346", - "version": 0, - "url": "https://ge.xhamster.com/videos/work-sex-1979-vintage-german-full-movie-better-quality-xhvwuh8", - "review": 0, - "should_download": 0, - "title": "Arbeitszeit (Germany 1979, full movie) | xHamster", - "file_name": "Arbeitszeit (Germany 1979, full movie) [xhvwuh8].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fc6531da-e50b-42f1-b87e-24e64634f999.mp4" - }, - { - "id": "fc7f64a7-604f-4c9c-bcaf-5ee8a75202a9", - "created_date": "2024-07-25 07:29:48.101694", - "last_modified_date": "2024-07-25 07:29:48.101694", - "version": 0, - "url": "https://ge.xhamster.com/videos/dont-worry-well-get-you-that-sperm-swapmom-jessica-starling-tells-swapdaughter-harley-king-s7-e3-xh0hShz", - "review": 0, - "should_download": 0, - "title": "\"Keine Sorgen, wir bekommen dich dieses Sperma!\" Swapmom jessica Starling sagt swapdaughter harley King-S7: E3 | xHamster", - "file_name": "\uff02Keine Sorgen, wir bekommen dich dieses Sperma!\uff02 Swapmom jessica Starling sagt swapdaughter harley King-S7\uff1a E3 [xh0hShz].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fc7f64a7-604f-4c9c-bcaf-5ee8a75202a9.mp4" - }, - { - "id": "fca48c73-0ffd-485c-86b3-0ae4f72d6b03", - "created_date": "2024-07-25 07:29:46.001109", - "last_modified_date": "2024-07-25 07:29:46.001109", - "version": 0, - "url": "https://ge.xhamster.com/videos/stossverkehr-im-schulbus-xhyJwKf", - "review": 0, - "should_download": 0, - "title": "Stossverkehr Im Schulbus, Free European Porn 53 | xHamster", - "file_name": "Stossverkehr Im Schulbus [xhyJwKf].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fca48c73-0ffd-485c-86b3-0ae4f72d6b03.mp4" - }, - { - "id": "fcb9ed0d-e45a-4eeb-a799-9168ef957564", - "created_date": "2024-11-01 21:08:26.933919", - "last_modified_date": "2024-11-01 21:08:26.933919", - "version": 0, - "url": "https://ge.xhamster.com/videos/wette-verloren-jetzt-ficken-wir-dich-xh8grFD", - "review": 0, - "should_download": 0, - "title": "Wette Verloren Jetzt Ficken Wir Dich, HD Porn 95 | xHamster", - "file_name": "Wette verloren jetzt ficken wir dich [xh8grFD].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/fcb9ed0d-e45a-4eeb-a799-9168ef957564.mp4" - }, - { - "id": "fd42defb-2033-4be7-8af6-d6f8322944a3", - "created_date": "2024-07-25 07:29:45.036684", - "last_modified_date": "2024-07-25 07:29:45.036684", - "version": 0, - "url": "https://ge.xhamster.desi/videos/mags-choice-1-8908123", - "review": 0, - "should_download": 0, - "title": "Mags Wahl 1 | xHamster", - "file_name": "Mags Wahl 1 [8908123].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/Media/fd42defb-2033-4be7-8af6-d6f8322944a3.mp4" - }, - { - "id": "fd5aa40b-dfa5-4b00-8efc-6d85785a9fa3", - "created_date": "2024-07-25 07:29:47.276456", - "last_modified_date": "2024-07-25 07:29:47.276456", - "version": 0, - "url": "https://ge.xhamster.com/videos/sommertraume-junger-madchen-nr-1-full-movie-xhOmiqY", - "review": 0, - "should_download": 0, - "title": "Sommertr\u00e4ume junger m\u00e4dchen # 1 (kompletter film) | xHamster", - "file_name": "Sommertr\u00e4ume junger m\u00e4dchen # 1 (kompletter film) [xhOmiqY].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fd5aa40b-dfa5-4b00-8efc-6d85785a9fa3.mp4" - }, - { - "id": "fd6c50f1-20f1-4387-94e9-f380f22eb202", - "created_date": "2024-07-25 07:29:45.786112", - "last_modified_date": "2024-07-25 07:29:45.786112", - "version": 0, - "url": "https://ge.xhamster.com/videos/schulmadchen-6-schamlos-nach-der-schule-xhd5zZ3", - "review": 0, - "should_download": 0, - "title": "Schulmadchen 6 - Schamlos Nach Der Schule: Free HD Porn 61 | xHamster", - "file_name": "Schulmadchen 6 - Schamlos Nach der Schule [xhd5zZ3].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fd6c50f1-20f1-4387-94e9-f380f22eb202.mp4" - }, - { - "id": "fd845e43-b85e-4903-9b04-6ee541d9adeb", - "created_date": "2024-07-25 07:29:46.629458", - "last_modified_date": "2024-07-25 07:29:46.629458", - "version": 0, - "url": "https://ge.xhamster.com/videos/cousin-pauline-1973-mkx-13752127", - "review": 0, - "should_download": 0, - "title": "Cousin Pauline - MKX | xHamster", - "file_name": "Cousin Pauline - MKX [13752127].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fd845e43-b85e-4903-9b04-6ee541d9adeb.mp4" - }, - { - "id": "fd86bdde-9954-423e-a0ce-11a33a03ba0a", - "created_date": "2024-07-25 07:29:46.572455", - "last_modified_date": "2024-07-25 07:29:46.572455", - "version": 0, - "url": "https://ge.xhamster.com/videos/fun-with-farmgirls-1993-13175749", - "review": 0, - "should_download": 0, - "title": "Spa\u00df mit Bauernm\u00e4dchen (1993) | xHamster", - "file_name": "Spa\u00df mit Bauernm\u00e4dchen (1993) [13175749].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fd86bdde-9954-423e-a0ce-11a33a03ba0a.mp4" - }, - { - "id": "fd98ad5f-961d-4b59-a6d8-c2d78e19508a", - "created_date": "2024-07-25 07:29:44.447845", - "last_modified_date": "2024-07-25 07:29:44.447845", - "version": 0, - "url": "https://ge.xhamster.com/videos/why-jerk-off-when-you-have-a-stepmom-like-cory-chase-tabooheat-xhMjY1b", - "review": 0, - "should_download": 0, - "title": "Warum wichsen, wenn du eine Stiefmutter wie Cory Chase hast? - Tabu-Hitze | xHamster", - "file_name": "Warum wichsen, wenn du eine Stiefmutter wie Cory Chase hast\uff1f - Tabu-Hitze [xhMjY1b].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fd98ad5f-961d-4b59-a6d8-c2d78e19508a.mp4" - }, - { - "id": "fddfbe31-3ab6-46bc-b7ca-3194431e6466", - "created_date": "2024-07-25 07:29:47.536387", - "last_modified_date": "2025-01-03 01:51:00.369000", - "version": 1, - "url": "https://ge.xhamster.com/videos/erotic-adventures-of-candy-1978-4k-remastered-14845665", - "review": 0, - "should_download": 0, - "title": "Erotische Abenteuer von Candy 1978 (4k remastered) | xHamster", - "file_name": "Erotische Abenteuer von Candy 1978 (4k remastered) [14845665].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fddfbe31-3ab6-46bc-b7ca-3194431e6466.mp4" - }, - { - "id": "fe2aa3f5-e79b-4667-9b3e-c80d43ef0626", - "created_date": "2024-11-10 16:53:33.490208", - "last_modified_date": "2024-11-10 16:53:33.490208", - "version": 0, - "url": "https://ge.xhamster.com/videos/sibling-seductions-3-xh8x5tQ", - "review": 0, - "should_download": 0, - "title": "Geschwisterverf\u00fchrungen 3 | xHamster", - "file_name": "Geschwisterverf\u00fchrungen 3 [xh8x5tQ].mp4", - "path": null, - "cloud_link": "/media/tpeetz/Media/fe2aa3f5-e79b-4667-9b3e-c80d43ef0626.mp4" - }, - { - "id": "fe2b8720-35ac-4740-a7b5-7e661e0c816d", - "created_date": "2024-07-25 07:29:45.763339", - "last_modified_date": "2024-07-25 07:29:45.763339", - "version": 0, - "url": "https://ge.xhamster.com/videos/threesome-on-the-beach-in-a-fantastic-island-xhjgrOy", - "review": 0, - "should_download": 0, - "title": "Dreier am Strand, auf einer fantastischen Insel | xHamster", - "file_name": "Dreier am Strand, auf einer fantastischen Insel [xhjgrOy].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fe2b8720-35ac-4740-a7b5-7e661e0c816d.mp4" - }, - { - "id": "fe3cb40d-c50e-4751-93de-ebfcdc987333", - "created_date": "2024-07-25 07:29:47.191322", - "last_modified_date": "2024-07-25 07:29:47.191322", - "version": 0, - "url": "https://ge.xhamster.com/videos/you-want-to-bang-my-stepdad-gfs-kinky-fantasy-xhlmUZe", - "review": 0, - "should_download": 0, - "title": "\"Du willst meinen Stiefvater knallen !?\" Freundin ist versaut, Fantasie | xHamster", - "file_name": "\uff02Du willst meinen Stiefvater knallen !\uff1f\uff02 Freundin ist versaut, Fantasie [xhlmUZe].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fe3cb40d-c50e-4751-93de-ebfcdc987333.mp4" - }, - { - "id": "fe8d2a23-56dd-44a2-9e9d-3c17227989f7", - "created_date": "2024-07-25 07:29:47.415040", - "last_modified_date": "2024-07-25 07:29:47.415040", - "version": 0, - "url": "https://ge.xhamster.com/videos/a-welcomed-distraction-xhog9CP", - "review": 0, - "should_download": 0, - "title": "Eine willkommene Ablenkung | xHamster", - "file_name": "Eine willkommene Ablenkung [xhog9CP].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fe8d2a23-56dd-44a2-9e9d-3c17227989f7.mp4" - }, - { - "id": "fecc5dd1-0fca-408d-a56a-527df13ad701", - "created_date": "2024-07-25 07:29:44.662671", - "last_modified_date": "2024-07-25 07:29:44.662671", - "version": 0, - "url": "https://ge.xhamster.com/videos/jung-und-geil-12203504", - "review": 0, - "should_download": 0, - "title": "Jung Und Geil: Free Pissing Porn Video 31 | xHamster", - "file_name": "Jung und geil [12203504].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fecc5dd1-0fca-408d-a56a-527df13ad701.mp4" - }, - { - "id": "feedde9d-0c65-4a08-b160-3c4b864d7368", - "created_date": "2024-07-25 07:29:45.233926", - "last_modified_date": "2024-07-25 07:29:45.233926", - "version": 0, - "url": "https://ge.xhamster.com/videos/meine-familie-ich-episode-4-xheKFK1", - "review": 0, - "should_download": 0, - "title": "Meine Familie Ich - Episode 4, Free Big Cock HD Porn 7a | xHamster", - "file_name": "Meine familie ich - Episode 4 [xheKFK1].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/feedde9d-0c65-4a08-b160-3c4b864d7368.mp4" - }, - { - "id": "fef798f2-119d-4cdb-bc02-7ab16b62ba31", - "created_date": "2024-07-25 07:29:45.491472", - "last_modified_date": "2024-07-25 07:29:45.491472", - "version": 0, - "url": "https://ge.xhamster.com/videos/busty-secretary-loves-riding-her-bosss-dick-5512216", - "review": 0, - "should_download": 0, - "title": "Vollbusige Sekret\u00e4rin liebt es, den Schwanz ihres Chefs zu reiten | xHamster", - "file_name": "Vollbusige Sekret\u00e4rin liebt es, den Schwanz ihres Chefs zu reiten [5512216].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fef798f2-119d-4cdb-bc02-7ab16b62ba31.mp4" - }, - { - "id": "ff1be3b4-9945-43ab-9abd-46573a9f4c8f", - "created_date": "2024-07-25 07:29:46.749984", - "last_modified_date": "2024-07-25 07:29:46.749984", - "version": 0, - "url": "https://ge.xhamster.com/videos/american-college-xxx-vol-2-original-version-in-hd-xhJRfPP", - "review": 0, - "should_download": 0, - "title": "American College xxx - vol (2) - (Originalversion in hd) | xHamster", - "file_name": "American College xxx - vol (2) - (Originalversion in hd) [xhJRfPP].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ff1be3b4-9945-43ab-9abd-46573a9f4c8f.mp4" - }, - { - "id": "ff6c572c-4985-4750-89b3-fa48d740b50d", - "created_date": "2024-07-25 07:29:44.525976", - "last_modified_date": "2024-07-25 07:29:44.525976", - "version": 0, - "url": "https://ge.xhamster.com/videos/german-amateur-threesome-with-creampie-2649148", - "review": 0, - "should_download": 0, - "title": "Deutscher Amateur-Dreier mit Creampie! | xHamster", - "file_name": "Deutscher Amateur-Dreier mit Creampie! [2649148].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ff6c572c-4985-4750-89b3-fa48d740b50d.mp4" - }, - { - "id": "ffa60223-391b-48df-9beb-97a6a59ad32a", - "created_date": "2024-07-25 07:29:45.832144", - "last_modified_date": "2024-07-25 07:29:45.832144", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-fleet-foursome-1992-xhIwCQP", - "review": 0, - "should_download": 0, - "title": "Der Flotte Vierer (1992) | xHamster", - "file_name": "Der Flotte Vierer (1992) [xhIwCQP].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ffa60223-391b-48df-9beb-97a6a59ad32a.mp4" - }, - { - "id": "ffb92eff-a5fb-4a63-9c26-a650e1737aed", - "created_date": "2024-07-25 07:29:46.553621", - "last_modified_date": "2024-07-25 07:29:46.553621", - "version": 0, - "url": "https://ge.xhamster.com/videos/the-young-like-it-hot-1983-9276329", - "review": 0, - "should_download": 0, - "title": "Die Jungen m\u00f6gen es hei\u00df 1983 | xHamster", - "file_name": "Die Jungen m\u00f6gen es hei\u00df 1983 [9276329].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ffb92eff-a5fb-4a63-9c26-a650e1737aed.mp4" - }, - { - "id": "ffc2f801-d77a-4b9b-9a84-825f626e7c05", - "created_date": "2024-07-25 07:29:48.169209", - "last_modified_date": "2024-07-25 07:29:48.169209", - "version": 0, - "url": "https://ge.xhamster.com/videos/no-one-told-me-it-would-be-so-fun-xhPCC1n", - "review": 0, - "should_download": 0, - "title": "Niemand sagte mir, es w\u00fcrde so viel Spa\u00df machen | xHamster", - "file_name": "Niemand sagte mir, es w\u00fcrde so viel Spa\u00df machen [xhPCC1n].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/ffc2f801-d77a-4b9b-9a84-825f626e7c05.mp4" - }, - { - "id": "ffddd1e3-3aa3-4f4f-b8b0-a588db6516d8", - "created_date": "2025-01-19 13:42:31.458322", - "last_modified_date": "2025-01-19 13:42:31.458329", - "version": 0, - "url": "https://ge.xhamster.com/videos/cheating-girlfriend-caught-in-the-act-two-guys-pleasuring-ellie-with-her-boyfriend-on-the-phone-xhxq2LF", - "review": 0, - "should_download": 0, - "title": "Betr\u00fcgende freundin auf frischer tat erwischt: Zwei typen befriedigen Ellie mit ihrem freund am telefon | xHamster", - "file_name": "Betr\u00fcgende freundin auf frischer tat erwischt\uff1a Zwei typen befriedigen Ellie mit ihrem freund am telefon [xhxq2LF].mp4", - "path": null, - "cloud_link": null - }, - { - "id": "fff3e63e-7a45-4a9f-b69b-ff6597390bf8", - "created_date": "2024-07-25 07:29:46.354059", - "last_modified_date": "2024-07-25 07:29:46.354059", - "version": 0, - "url": "https://ge.xhamster.com/videos/alyssia-kent-gerson-denny-rained-out-babes-13199158", - "review": 0, - "should_download": 0, - "title": "Alyssia Kent Gerson Denny - geregnet - Sch\u00e4tzchen | xHamster", - "file_name": "Alyssia Kent Gerson Denny - geregnet - Sch\u00e4tzchen [13199158].mp4", - "path": "/mnt/e/media", - "cloud_link": "/media/tpeetz/media1/fff3e63e-7a45-4a9f-b69b-ff6597390bf8.mp4" - } - ], - "article_author": [], - "book": [], - "role": [ - { - "id": "05a186f6-36a2-4cce-8904-187301193937", - "created_date": "2024-10-21 15:20:05.848000", - "last_modified_date": "2024-10-21 15:20:05.848000", - "version": 0, - "name": "MEDIA" - }, - { - "id": "0e0a5291-e74b-4ef8-823a-103f731e58bf", - "created_date": "2024-08-16 23:58:34.457000", - "last_modified_date": "2024-08-16 23:58:34.457000", - "version": 0, - "name": "ROLE_USER" - }, - { - "id": "d975bc73-1c21-4344-a500-4a90440edf7d", - "created_date": "2024-08-16 23:58:37.140000", - "last_modified_date": "2024-08-16 23:58:37.140000", - "version": 0, - "name": "ROLE_MEDIA" - }, - { - "id": "dd6bad92-8ff0-4928-93ab-b356c75f2a81", - "created_date": "2024-08-16 23:58:34.408000", - "last_modified_date": "2024-08-16 23:58:34.408000", - "version": 0, - "name": "ROLE_ADMIN" - } - ], - "field_position": [ - { - "id": "0968459a-b649-4ed7-a3ee-c14172e93b0e", - "created_date": "2024-08-16 23:58:37.437000", - "last_modified_date": "2024-08-16 23:58:37.437000", - "version": 0, - "name": "Goalie", - "short_name": "G", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "0a2976b2-a6f5-4e87-86e0-97270f64958d", - "created_date": "2024-08-16 23:58:37.363000", - "last_modified_date": "2024-08-16 23:58:37.363000", - "version": 0, - "name": "Tight End", - "short_name": "TE", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "1a363e8b-c34c-4ef9-9677-6fd7b9bdf143", - "created_date": "2024-08-16 23:58:37.374000", - "last_modified_date": "2024-08-16 23:58:37.374000", - "version": 0, - "name": "Kicker", - "short_name": "K", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "1bb73f00-21d3-4fca-a5c5-7f31be3670a1", - "created_date": "2024-08-16 23:58:37.382000", - "last_modified_date": "2024-08-16 23:58:37.382000", - "version": 0, - "name": "Long Snapper", - "short_name": "LS", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "2561c3e9-e09b-42f3-98fc-7224ce8ea85e", - "created_date": "2024-08-16 23:58:37.443000", - "last_modified_date": "2024-08-16 23:58:37.443000", - "version": 0, - "name": "Center", - "short_name": "C", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "2c33f079-cfaf-44ab-91ae-d03411ba745b", - "created_date": "2024-08-16 23:58:37.378000", - "last_modified_date": "2024-08-16 23:58:37.378000", - "version": 0, - "name": "Kick Returner", - "short_name": "KR", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "32bae8cd-c01e-4a13-82cc-2e6699bb30b9", - "created_date": "2024-08-16 23:58:37.421000", - "last_modified_date": "2024-08-16 23:58:37.421000", - "version": 0, - "name": "Third Base", - "short_name": "3B", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "3303b76e-688c-4a4c-8f7e-f76b1df5f5de", - "created_date": "2024-08-16 23:58:37.371000", - "last_modified_date": "2024-08-16 23:58:37.371000", - "version": 0, - "name": "Defensive Back", - "short_name": "DB", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "3682a19c-52de-40d5-9307-126d4c579d69", - "created_date": "2024-08-16 23:58:37.361000", - "last_modified_date": "2024-08-16 23:58:37.361000", - "version": 0, - "name": "Wide Receiver", - "short_name": "WR", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "370a5eb5-07c1-4ebf-94a0-57377a395fe1", - "created_date": "2024-08-16 23:58:37.375000", - "last_modified_date": "2024-08-16 23:58:37.375000", - "version": 0, - "name": "Punter", - "short_name": "P", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "3b57d6a5-004f-4bbf-9c8f-81e58084a919", - "created_date": "2024-08-16 23:58:37.411000", - "last_modified_date": "2024-08-16 23:58:37.411000", - "version": 0, - "name": "Outside Linebacker", - "short_name": "OLB", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "4408073f-29ef-4910-be1f-a9979ec96005", - "created_date": "2024-08-16 23:58:37.427000", - "last_modified_date": "2024-08-16 23:58:37.427000", - "version": 0, - "name": "Right Field", - "short_name": "RF", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "4632ca65-3155-4cb1-ad56-6886b12ee99a", - "created_date": "2024-08-16 23:58:37.438000", - "last_modified_date": "2024-08-16 23:58:37.438000", - "version": 0, - "name": "Defense", - "short_name": "D", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "47d394b9-f0de-4eb5-a5e0-afcb26506810", - "created_date": "2024-08-16 23:58:37.414000", - "last_modified_date": "2024-08-16 23:58:37.414000", - "version": 0, - "name": "Strong Safety", - "short_name": "SS", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "54aedda1-1732-4f14-98f4-0aecc924df15", - "created_date": "2024-08-16 23:58:37.364000", - "last_modified_date": "2024-08-16 23:58:37.364000", - "version": 0, - "name": "Fullback", - "short_name": "FB", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "598b3b35-6aa7-4aee-8299-f027bac4848b", - "created_date": "2024-08-16 23:58:37.406000", - "last_modified_date": "2024-08-16 23:58:37.406000", - "version": 0, - "name": "Cornerback", - "short_name": "CB", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "66623356-c8ce-4d7f-8e93-8c950fc28a0b", - "created_date": "2024-08-16 23:58:37.431000", - "last_modified_date": "2024-08-16 23:58:37.431000", - "version": 0, - "name": "Shooting Guard", - "short_name": "SG", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "69904927-8308-4099-910f-a786163334b9", - "created_date": "2024-08-16 23:58:37.426000", - "last_modified_date": "2024-08-16 23:58:37.426000", - "version": 0, - "name": "Center Field", - "short_name": "CF", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "73f6ded0-d803-4125-9adc-2c3cca3d4019", - "created_date": "2024-08-16 23:58:37.434000", - "last_modified_date": "2024-08-16 23:58:37.434000", - "version": 0, - "name": "Power Forward", - "short_name": "PF", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "7d265c03-d616-491b-a354-b1bda4bd410b", - "created_date": "2024-08-16 23:58:37.384000", - "last_modified_date": "2024-08-16 23:58:37.384000", - "version": 0, - "name": "Right Guard", - "short_name": "RG", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "90f07094-5f90-41cb-99f3-70efaf34c68e", - "created_date": "2024-08-16 23:58:37.435000", - "last_modified_date": "2024-08-16 23:58:37.435000", - "version": 0, - "name": "Center", - "short_name": "C", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "912415e0-6eb8-4baa-8e38-58514b49816a", - "created_date": "2024-08-16 23:58:37.429000", - "last_modified_date": "2024-08-16 23:58:37.429000", - "version": 0, - "name": "Point Guard", - "short_name": "PG", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "99e770eb-66c4-4b4d-bd95-d32ed7b818fd", - "created_date": "2024-08-16 23:58:37.441000", - "last_modified_date": "2024-08-16 23:58:37.441000", - "version": 0, - "name": "Right Wing", - "short_name": "RW", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "a313bf86-1347-42c4-83c0-fe3bd0b15aa0", - "created_date": "2024-08-16 23:58:37.412000", - "last_modified_date": "2024-08-16 23:58:37.412000", - "version": 0, - "name": "Inside Linebacker", - "short_name": "ILB", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "a37ab055-d340-4d6c-8fb3-2262c2372a42", - "created_date": "2024-08-16 23:58:37.417000", - "last_modified_date": "2024-08-16 23:58:37.417000", - "version": 0, - "name": "Catcher", - "short_name": "C", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "a5e483be-3f8a-44b9-81d8-4f767ba9a417", - "created_date": "2024-08-16 23:58:37.367000", - "last_modified_date": "2024-08-16 23:58:37.367000", - "version": 0, - "name": "Offensive Line", - "short_name": "OL", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "acb284fb-8917-4fbd-bd76-861a2636a3bc", - "created_date": "2024-08-16 23:58:37.424000", - "last_modified_date": "2024-08-16 23:58:37.424000", - "version": 0, - "name": "Left Field", - "short_name": "LF", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "adf598a1-d6b8-4b70-8d56-502cf731e95c", - "created_date": "2024-08-16 23:58:37.379000", - "last_modified_date": "2024-08-16 23:58:37.379000", - "version": 0, - "name": "Punt Returner", - "short_name": "PR", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "b43cdc34-5227-43fa-92f6-65c7ac5f6a64", - "created_date": "2024-08-16 23:58:37.432000", - "last_modified_date": "2024-08-16 23:58:37.432000", - "version": 0, - "name": "Small Forward", - "short_name": "SF", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "b4847bf2-1e29-4fa3-9f17-a3ae16c6c833", - "created_date": "2024-08-16 23:58:37.408000", - "last_modified_date": "2024-08-16 23:58:37.408000", - "version": 0, - "name": "Defensive Tackle", - "short_name": "DT", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "bcb2f1ee-d2e4-4131-a19f-4fc11a2c674b", - "created_date": "2024-08-16 23:58:37.386000", - "last_modified_date": "2024-08-16 23:58:37.386000", - "version": 0, - "name": "Offensive Tackle", - "short_name": "OF", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "c5f221ec-4ed7-4fd6-a761-6177ff3661c0", - "created_date": "2024-08-16 23:58:37.377000", - "last_modified_date": "2024-08-16 23:58:37.377000", - "version": 0, - "name": "Safety", - "short_name": "S", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "cd7e15d3-2d95-415b-9dfe-18ca3ed53a26", - "created_date": "2024-08-16 23:58:37.369000", - "last_modified_date": "2024-08-16 23:58:37.369000", - "version": 0, - "name": "Linebacker", - "short_name": "LB", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "cea8dc5a-e9c6-4f52-8644-3f3e1a4ddefe", - "created_date": "2024-08-16 23:58:37.416000", - "last_modified_date": "2024-08-16 23:58:37.416000", - "version": 0, - "name": "Pitcher", - "short_name": "P", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "cf03e12d-4c17-4165-8ede-d30107c4c4ae", - "created_date": "2024-08-16 23:58:37.418000", - "last_modified_date": "2024-08-16 23:58:37.418000", - "version": 0, - "name": "First Base", - "short_name": "1B", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "d113f005-7792-494d-ad98-b1ce9a30130c", - "created_date": "2024-08-16 23:58:37.359000", - "last_modified_date": "2024-08-16 23:58:37.359000", - "version": 0, - "name": "Running Back", - "short_name": "RB", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "d200e02a-5ae5-44a0-adab-cb11d07ad0e7", - "created_date": "2024-08-16 23:58:37.420000", - "last_modified_date": "2024-08-16 23:58:37.420000", - "version": 0, - "name": "Second Base", - "short_name": "2B", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "dc2034e1-eb61-4741-b16d-b1267ce21df4", - "created_date": "2024-08-16 23:58:37.368000", - "last_modified_date": "2024-08-16 23:58:37.368000", - "version": 0, - "name": "Defensive Line", - "short_name": "DL", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "dcf3cda6-3357-4878-ba9a-fe3e2b3b89e7", - "created_date": "2024-08-16 23:58:37.440000", - "last_modified_date": "2024-08-16 23:58:37.440000", - "version": 0, - "name": "Left Wing", - "short_name": "LW", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "e4201d7e-fe49-439e-ad2e-e95f27f05ed5", - "created_date": "2024-08-16 23:58:37.354000", - "last_modified_date": "2024-08-16 23:58:37.354000", - "version": 0, - "name": "Quarterback", - "short_name": "QB", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "eef704c2-2979-49cc-962f-cbd35574fbf9", - "created_date": "2024-08-16 23:58:37.409000", - "last_modified_date": "2024-08-16 23:58:37.409000", - "version": 0, - "name": "Nose Tackle", - "short_name": "NT", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "f01e852c-7e36-4365-9c68-2aa9c809a881", - "created_date": "2024-08-16 23:58:37.423000", - "last_modified_date": "2024-08-16 23:58:37.423000", - "version": 0, - "name": "Shortstop", - "short_name": "SS", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "f1d415bb-83dc-4fa4-baf3-a6102520b995", - "created_date": "2024-08-16 23:58:37.383000", - "last_modified_date": "2024-08-16 23:58:37.383000", - "version": 0, - "name": "Left Guard", - "short_name": "LG", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "f3fc447f-497f-4f98-b2ce-0f6e7141096a", - "created_date": "2024-08-16 23:58:37.372000", - "last_modified_date": "2024-08-16 23:58:37.372000", - "version": 0, - "name": "Defensive End", - "short_name": "DE", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - } - ], - "card_set": [ - { - "id": "0a2a52ab-93c1-49c2-b2f6-abf3b9c2c3ae", - "created_date": "2024-08-16 23:58:37.470000", - "last_modified_date": "2024-08-16 23:58:37.470000", - "version": 0, - "insert_set": 0, - "parallel_set": 1, - "name": "Pacific Copper", - "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" - }, - { - "id": "22bc331b-e604-46e5-9958-2e65489033d6", - "created_date": "2024-08-16 23:58:37.482000", - "last_modified_date": "2024-08-16 23:58:37.482000", - "version": 0, - "insert_set": 0, - "parallel_set": 0, - "name": "Finest Hour", - "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" - }, - { - "id": "61c2f801-1301-4e6e-8628-550f655a7f4d", - "created_date": "2024-08-16 23:58:37.481000", - "last_modified_date": "2024-08-16 23:58:37.481000", - "version": 0, - "insert_set": 0, - "parallel_set": 0, - "name": "Mystique", - "vendor_id": "d7617bf3-056a-489b-8563-628f2dd3da84" - }, - { - "id": "63edf46e-30cc-4378-92fd-ce5bdc83a035", - "created_date": "2024-08-16 23:58:37.473000", - "last_modified_date": "2024-08-16 23:58:37.473000", - "version": 0, - "insert_set": 0, - "parallel_set": 0, - "name": "Pacific", - "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" - }, - { - "id": "6a1e1f44-28d8-4aaf-b8ee-9043115ae2ca", - "created_date": "2024-08-16 23:58:37.469000", - "last_modified_date": "2024-08-16 23:58:37.469000", - "version": 0, - "insert_set": 0, - "parallel_set": 1, - "name": "Mystique Gold", - "vendor_id": "d7617bf3-056a-489b-8563-628f2dd3da84" - }, - { - "id": "6f5bba5b-e3bd-41f7-9f37-3275057c31d2", - "created_date": "2024-08-16 23:58:37.479000", - "last_modified_date": "2024-08-16 23:58:37.479000", - "version": 0, - "insert_set": 0, - "parallel_set": 0, - "name": "Ultra", - "vendor_id": "d7617bf3-056a-489b-8563-628f2dd3da84" - }, - { - "id": "790e00ea-1434-45d0-86d2-9ba3beca5570", - "created_date": "2024-08-16 23:58:37.476000", - "last_modified_date": "2024-08-16 23:58:37.476000", - "version": 0, - "insert_set": 0, - "parallel_set": 0, - "name": "Bowman", - "vendor_id": "eb42fc0c-20ad-4e5b-b99a-552ac861db05" - }, - { - "id": "8cbb110c-9158-4ce4-a505-46e7af931f8f", - "created_date": "2024-08-16 23:58:37.465000", - "last_modified_date": "2024-08-16 23:58:37.465000", - "version": 0, - "insert_set": 1, - "parallel_set": 0, - "name": "Mystique Big Buzz", - "vendor_id": "d7617bf3-056a-489b-8563-628f2dd3da84" - }, - { - "id": "aa6b5458-5805-4858-807d-4e07e3138053", - "created_date": "2024-08-16 23:58:37.483000", - "last_modified_date": "2024-08-16 23:58:37.483000", - "version": 0, - "insert_set": 0, - "parallel_set": 0, - "name": "SP", - "vendor_id": "6472c0a1-2b4f-440f-813d-66ea3540f78c" - }, - { - "id": "b8a6e929-7dbd-439b-be79-4dd7f45d94fb", - "created_date": "2024-08-16 23:58:37.474000", - "last_modified_date": "2024-08-16 23:58:37.474000", - "version": 0, - "insert_set": 0, - "parallel_set": 0, - "name": "Fleer", - "vendor_id": "d7617bf3-056a-489b-8563-628f2dd3da84" - }, - { - "id": "bd416669-0d94-481c-96c4-6422b028fc9b", - "created_date": "2024-08-16 23:58:37.487000", - "last_modified_date": "2024-08-16 23:58:37.487000", - "version": 0, - "insert_set": 0, - "parallel_set": 0, - "name": "SP Authentic", - "vendor_id": "6472c0a1-2b4f-440f-813d-66ea3540f78c" - }, - { - "id": "ca49cae9-8597-4fce-91b9-fac6dd17bb63", - "created_date": "2024-08-16 23:58:37.485000", - "last_modified_date": "2024-08-16 23:58:37.485000", - "version": 0, - "insert_set": 0, - "parallel_set": 0, - "name": "SPX", - "vendor_id": "6472c0a1-2b4f-440f-813d-66ea3540f78c" - }, - { - "id": "cd4b5a4f-9200-4d12-acfd-006842d6b823", - "created_date": "2024-08-16 23:58:37.478000", - "last_modified_date": "2024-08-16 23:58:37.478000", - "version": 0, - "insert_set": 0, - "parallel_set": 0, - "name": "Leaf", - "vendor_id": "5e0f3f5d-c58b-4517-b30b-e767c34404ab" - }, - { - "id": "d7d83654-66c2-4c85-82f1-f10a7d1f3c7c", - "created_date": "2024-08-16 23:58:37.472000", - "last_modified_date": "2024-08-16 23:58:37.472000", - "version": 0, - "insert_set": 0, - "parallel_set": 1, - "name": "Pacific Gold", - "vendor_id": "5d409413-db78-43b3-bc7a-a76f5718c433" - }, - { - "id": "db3d6628-d789-4c24-822d-b5b9e1f8c0ab", - "created_date": "2024-08-16 23:58:37.488000", - "last_modified_date": "2024-08-16 23:58:37.488000", - "version": 0, - "insert_set": 0, - "parallel_set": 0, - "name": "Black Diamond", - "vendor_id": "6472c0a1-2b4f-440f-813d-66ea3540f78c" - } - ], - "team": [ - { - "id": "001a6867-9629-4314-ae56-78c2c267e572", - "created_date": "2024-08-16 23:58:37.272000", - "last_modified_date": "2024-08-16 23:58:37.272000", - "version": 0, - "name": "Chicago Bulls", - "short_name": "Bulls", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "035883e2-cc5c-40ae-9efb-9bdc0cc77199", - "created_date": "2024-08-16 23:58:37.266000", - "last_modified_date": "2024-08-16 23:58:37.266000", - "version": 0, - "name": "Philadelphia 76ers", - "short_name": "76ers", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "0839c619-4378-41dd-b5c2-3d398ee46870", - "created_date": "2024-08-16 23:58:37.348000", - "last_modified_date": "2024-08-16 23:58:37.348000", - "version": 0, - "name": "Phoenix Coyotes", - "short_name": "Coyotes", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "0a193e30-2e93-4747-a544-762eee9a9f12", - "created_date": "2024-08-16 23:58:37.310000", - "last_modified_date": "2024-08-16 23:58:37.310000", - "version": 0, - "name": "New Jersey Devils", - "short_name": "Devils", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "0c1d90c1-a0cb-4f58-94e6-58a419e4d8b8", - "created_date": "2024-08-16 23:58:37.173000", - "last_modified_date": "2024-08-16 23:58:37.173000", - "version": 0, - "name": "Cleveland Browns", - "short_name": "Browns", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "0ca47c2d-4f65-4c49-8882-8a2faa314b94", - "created_date": "2024-08-16 23:58:37.229000", - "last_modified_date": "2024-08-16 23:58:37.229000", - "version": 0, - "name": "Texas Rangers", - "short_name": "Rangers", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "117fbd2e-4fa5-4c09-bf19-10cfc59cff70", - "created_date": "2024-08-16 23:58:37.217000", - "last_modified_date": "2024-08-16 23:58:37.217000", - "version": 0, - "name": "Chicago White Sox", - "short_name": "White Sox", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "13c2ba23-a72a-49f4-9674-725c5b80a909", - "created_date": "2024-08-16 23:58:37.202000", - "last_modified_date": "2024-08-16 23:58:37.202000", - "version": 0, - "name": "Carolina Panthers", - "short_name": "Panthers", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "179f15e0-6628-40a0-b79d-30fffa5445b2", - "created_date": "2024-08-16 23:58:37.192000", - "last_modified_date": "2024-08-16 23:58:37.192000", - "version": 0, - "name": "Washington Redskins", - "short_name": "Redskins", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "1965b670-2125-4d3e-a71e-437895a14bd5", - "created_date": "2024-08-16 23:58:37.203000", - "last_modified_date": "2024-08-16 23:58:37.203000", - "version": 0, - "name": "New Orleans Saints", - "short_name": "Saints", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "1997c461-fd71-498a-ac29-590e4189c4ee", - "created_date": "2024-08-16 23:58:37.223000", - "last_modified_date": "2024-08-16 23:58:37.223000", - "version": 0, - "name": "Minnesota Twins", - "short_name": "Twins", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "1af4cb19-3507-4630-9eff-c97a81e422c2", - "created_date": "2024-08-16 23:58:37.177000", - "last_modified_date": "2024-08-16 23:58:37.177000", - "version": 0, - "name": "Tennessee Titans", - "short_name": "Titans", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "1cdf0d19-2ed8-471c-b689-cca82c557a5d", - "created_date": "2024-08-16 23:58:37.169000", - "last_modified_date": "2024-08-16 23:58:37.169000", - "version": 0, - "name": "Baltimore Ravens", - "short_name": "Ravens", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "1f184a5b-6448-4845-aa39-b84c4b566603", - "created_date": "2024-08-16 23:58:37.159000", - "last_modified_date": "2024-08-16 23:58:37.159000", - "version": 0, - "name": "Buffalo Bills", - "short_name": "Bills", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "217cc98f-be07-4b3c-8159-a0fd2b0957a0", - "created_date": "2024-08-16 23:58:37.268000", - "last_modified_date": "2024-08-16 23:58:37.268000", - "version": 0, - "name": "Washington Wizards", - "short_name": "Wizards", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "2394bfef-6a40-4430-9966-64cfc9a886ba", - "created_date": "2024-08-16 23:58:37.322000", - "last_modified_date": "2024-08-16 23:58:37.322000", - "version": 0, - "name": "Florida Panthers", - "short_name": "Panthers", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "25f52b52-2d3d-426a-8761-c24e5191bd8b", - "created_date": "2024-08-16 23:58:37.303000", - "last_modified_date": "2024-08-16 23:58:37.303000", - "version": 0, - "name": "Boston Bruins", - "short_name": "Bruins", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "2666f96d-7f3d-47cf-a901-3fde1dcde8b0", - "created_date": "2024-08-16 23:58:37.214000", - "last_modified_date": "2024-08-16 23:58:37.214000", - "version": 0, - "name": "Tampa Bay Devil Rays", - "short_name": "Devil Rays", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "29805313-e491-4263-ac98-5bfd009669ee", - "created_date": "2024-08-16 23:58:37.164000", - "last_modified_date": "2024-08-16 23:58:37.164000", - "version": 0, - "name": "Miami Dolphins", - "short_name": "Dolphins", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "2b7fd89f-f3dd-4a01-a494-c755cf7859fe", - "created_date": "2024-08-16 23:58:37.174000", - "last_modified_date": "2024-08-16 23:58:37.174000", - "version": 0, - "name": "Jacksonville Jaguars", - "short_name": "Jaguars", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "2bdf925d-f3ad-4692-aae1-d0bbba16c339", - "created_date": "2024-08-16 23:58:37.194000", - "last_modified_date": "2024-08-16 23:58:37.194000", - "version": 0, - "name": "Detroit Lions", - "short_name": "Lions", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "2c18d74c-892c-45e0-bd4c-b2c94aa5e0bc", - "created_date": "2024-08-16 23:58:37.255000", - "last_modified_date": "2024-08-16 23:58:37.255000", - "version": 0, - "name": "San Francisco Giants", - "short_name": "Giants", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "2c378b13-177a-4fef-aa09-688d8f501585", - "created_date": "2024-08-16 23:58:37.196000", - "last_modified_date": "2024-08-16 23:58:37.196000", - "version": 0, - "name": "Green Bay Packers", - "short_name": "Packers", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "2d890234-64dd-45ca-9155-cd1accc8afd1", - "created_date": "2024-08-16 23:58:37.280000", - "last_modified_date": "2024-08-16 23:58:37.280000", - "version": 0, - "name": "Toronto Raptors", - "short_name": "Raptors", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "2d9897fc-d80e-4eb9-aac9-db36b434bb43", - "created_date": "2024-08-16 23:58:37.186000", - "last_modified_date": "2024-08-16 23:58:37.186000", - "version": 0, - "name": "Arizona Cardinals", - "short_name": "Cardinals", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "2f0554e3-54c2-434f-b22a-f94c8dc32c23", - "created_date": "2024-08-16 23:58:37.279000", - "last_modified_date": "2024-08-16 23:58:37.279000", - "version": 0, - "name": "Milwaukee Bucks", - "short_name": "Bucks", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "323d2a5a-75c2-4636-9101-df353e25eaa4", - "created_date": "2024-08-16 23:58:37.304000", - "last_modified_date": "2024-08-16 23:58:37.304000", - "version": 0, - "name": "Buffalo Sabres", - "short_name": "Sabres", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "39ad73fd-1e5c-4f4f-9d17-32e01aaa7526", - "created_date": "2024-08-16 23:58:37.191000", - "last_modified_date": "2024-08-16 23:58:37.191000", - "version": 0, - "name": "Philadelphia Eagles", - "short_name": "Eagles", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "3d0e3c9c-7b41-4ed8-83ea-a45a037a6afb", - "created_date": "2024-08-16 23:58:37.188000", - "last_modified_date": "2024-08-16 23:58:37.188000", - "version": 0, - "name": "New York Giants", - "short_name": "Giants", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "3eb84f3e-fce5-4a06-8f60-e08107740ab8", - "created_date": "2024-08-16 23:58:37.172000", - "last_modified_date": "2024-08-16 23:58:37.172000", - "version": 0, - "name": "Cincinnati Bengals", - "short_name": "Bengals", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "3fec6af3-d87d-4dd5-b15c-1c6982135920", - "created_date": "2024-08-16 23:58:37.176000", - "last_modified_date": "2024-08-16 23:58:37.176000", - "version": 0, - "name": "Pittsburgh Steelers", - "short_name": "Steelers", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "4050759a-ece4-4c81-90a3-85d576eb4fbd", - "created_date": "2024-08-16 23:58:37.326000", - "last_modified_date": "2024-08-16 23:58:37.326000", - "version": 0, - "name": "Chicago Blackhawks", - "short_name": "Blackhawks", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "427383d4-203b-44b7-ad5e-6fc8026dce39", - "created_date": "2024-08-16 23:58:37.292000", - "last_modified_date": "2024-08-16 23:58:37.292000", - "version": 0, - "name": "Golden State Warriors", - "short_name": "Warriors", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "44158247-f016-4d95-8f1f-bd888b6ab4e8", - "created_date": "2024-08-16 23:58:37.243000", - "last_modified_date": "2024-08-16 23:58:37.243000", - "version": 0, - "name": "Pittsburgh Pirates", - "short_name": "Pirates", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "450677ba-37de-4255-a23d-0e9b896d4bd7", - "created_date": "2024-08-16 23:58:37.320000", - "last_modified_date": "2024-08-16 23:58:37.320000", - "version": 0, - "name": "Carolina Hurricanes", - "short_name": "Hurricanes", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "47bfdb12-7874-4245-bb85-1302359c494b", - "created_date": "2024-08-16 23:58:37.290000", - "last_modified_date": "2024-08-16 23:58:37.290000", - "version": 0, - "name": "Utah Jazz", - "short_name": "Jazz", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "48b372f7-b568-4f64-9f81-8cd770da7a8d", - "created_date": "2024-08-16 23:58:37.312000", - "last_modified_date": "2024-08-16 23:58:37.312000", - "version": 0, - "name": "New York Islanders", - "short_name": "Islanders", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "4a0a308b-2b0a-44c3-8d52-c90ffa660a1a", - "created_date": "2024-08-16 23:58:37.187000", - "last_modified_date": "2024-08-16 23:58:37.187000", - "version": 0, - "name": "Dallas Cowboys", - "short_name": "Cowboys", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "4c39f899-1653-406a-9780-e74b6266559a", - "created_date": "2024-08-16 23:58:37.227000", - "last_modified_date": "2024-08-16 23:58:37.227000", - "version": 0, - "name": "Seattle Mariners", - "short_name": "Mariners", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "4f4bdba0-ba5a-4f52-9b60-fd136c094aed", - "created_date": "2024-08-16 23:58:37.219000", - "last_modified_date": "2024-08-16 23:58:37.219000", - "version": 0, - "name": "Cleveland Indians", - "short_name": "Indians", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "4fecdfc3-4ee3-41ee-8352-9c6d64934105", - "created_date": "2024-08-16 23:58:37.262000", - "last_modified_date": "2024-08-16 23:58:37.262000", - "version": 0, - "name": "New York Knicks", - "short_name": "Knicks", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "5002d98a-6074-489a-9b47-452cb5451e7c", - "created_date": "2024-08-16 23:58:37.323000", - "last_modified_date": "2024-08-16 23:58:37.323000", - "version": 0, - "name": "Tampa Bay Lightnings", - "short_name": "Lightnings", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "50b4c394-0377-41f1-a976-13919fe3b608", - "created_date": "2024-08-16 23:58:37.256000", - "last_modified_date": "2024-08-16 23:58:37.256000", - "version": 0, - "name": "Boston Celtics", - "short_name": "Celtics", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "575a9788-621e-40f3-ad15-16750f545ab8", - "created_date": "2024-08-16 23:58:37.234000", - "last_modified_date": "2024-08-16 23:58:37.234000", - "version": 0, - "name": "New York Mets", - "short_name": "Mets", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "57ac5188-be46-4c20-87ba-bfaf24d9f9c1", - "created_date": "2024-08-16 23:58:37.331000", - "last_modified_date": "2024-08-16 23:58:37.331000", - "version": 0, - "name": "Nashville Predators", - "short_name": "Predators", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "5c8137a0-0483-4a34-9c37-5e59c7d34665", - "created_date": "2024-08-16 23:58:37.273000", - "last_modified_date": "2024-08-16 23:58:37.273000", - "version": 0, - "name": "Cleveland Cavaliers", - "short_name": "Cavaliers", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "5f753766-dff8-42ed-bf6d-ae05fd4353f4", - "created_date": "2024-08-16 23:58:37.212000", - "last_modified_date": "2024-08-16 23:58:37.212000", - "version": 0, - "name": "Boston Red Sox", - "short_name": "Red Sox", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "5f7fa954-cce0-4227-8ba4-689803583a30", - "created_date": "2024-08-16 23:58:37.283000", - "last_modified_date": "2024-08-16 23:58:37.283000", - "version": 0, - "name": "Denver Nuggets", - "short_name": "Nuggets", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "5f8d02c0-5c86-4ed7-99c2-c9ce113c73e2", - "created_date": "2024-08-16 23:58:37.308000", - "last_modified_date": "2024-08-16 23:58:37.308000", - "version": 0, - "name": "Toronto Maple Leafs", - "short_name": "Maple Leafs", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "6005c069-289d-4c07-8e5e-6cd77c76144a", - "created_date": "2024-08-16 23:58:37.341000", - "last_modified_date": "2024-08-16 23:58:37.341000", - "version": 0, - "name": "Anaheim Mighty Ducks", - "short_name": "Mighty Ducks", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "620bdeff-ff03-4837-b6df-ee5b9e4e8c71", - "created_date": "2024-08-16 23:58:37.213000", - "last_modified_date": "2024-08-16 23:58:37.213000", - "version": 0, - "name": "New York Yankees", - "short_name": "Yankees", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "6282602e-8c75-4e10-980e-fc296fced8ec", - "created_date": "2024-08-16 23:58:37.204000", - "last_modified_date": "2024-08-16 23:58:37.204000", - "version": 0, - "name": "St.Louis Rams", - "short_name": "Rams", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "63146d4c-bada-47ba-9d3e-bce28dd89baa", - "created_date": "2024-08-16 23:58:37.327000", - "last_modified_date": "2024-08-16 23:58:37.327000", - "version": 0, - "name": "Columbus Blue Jackets", - "short_name": "Blue Jackets", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "633c0ae8-979d-4a3f-a31a-0304a2552263", - "created_date": "2024-08-16 23:58:37.277000", - "last_modified_date": "2024-08-16 23:58:37.277000", - "version": 0, - "name": "Indiana Pacers", - "short_name": "Pacers", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "639578fd-9a05-4c7b-b0ad-6964939c3f50", - "created_date": "2024-08-16 23:58:37.345000", - "last_modified_date": "2024-08-16 23:58:37.345000", - "version": 0, - "name": "Dallas Stars", - "short_name": "Stars", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "63e7a35f-24e7-44bd-bebd-c8995a21da09", - "created_date": "2024-08-16 23:58:37.236000", - "last_modified_date": "2024-08-16 23:58:37.236000", - "version": 0, - "name": "Philadelphia Phillies", - "short_name": "Phillies", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "66ea5a89-f433-4192-9e90-7409e5581902", - "created_date": "2024-08-16 23:58:37.269000", - "last_modified_date": "2024-08-16 23:58:37.269000", - "version": 0, - "name": "Atlanta Hawks", - "short_name": "Hawks", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "6c081833-0558-4ec3-8311-5bc7aced52cc", - "created_date": "2024-08-16 23:58:37.264000", - "last_modified_date": "2024-08-16 23:58:37.264000", - "version": 0, - "name": "Orlando Magic", - "short_name": "Magic", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "6c318b96-214b-42ad-bec9-98cf2c02f190", - "created_date": "2024-08-16 23:58:37.333000", - "last_modified_date": "2024-08-16 23:58:37.333000", - "version": 0, - "name": "Calgary Flames", - "short_name": "Flames", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "6c6d740f-3f1c-49b2-b78d-7ec1b18a704c", - "created_date": "2024-08-16 23:58:37.316000", - "last_modified_date": "2024-08-16 23:58:37.316000", - "version": 0, - "name": "Pittsburgh Penguins", - "short_name": "Penguins", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "713f56be-1d53-426c-9c9d-77fe5c5c7368", - "created_date": "2024-08-16 23:58:37.334000", - "last_modified_date": "2024-08-16 23:58:37.334000", - "version": 0, - "name": "Colorado Avalanche", - "short_name": "Avalanche", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "760bea8c-530b-4a0f-a8d4-05e7b8311713", - "created_date": "2024-08-16 23:58:37.197000", - "last_modified_date": "2024-08-16 23:58:37.197000", - "version": 0, - "name": "Minnesota Vikings", - "short_name": "Vikings", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "76278aa5-7c0f-4963-a03f-c3d768fc45ad", - "created_date": "2024-08-16 23:58:37.210000", - "last_modified_date": "2024-08-16 23:58:37.210000", - "version": 0, - "name": "Baltimore Orioles", - "short_name": "Orioles", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "762d7f66-18d6-47f4-b59d-e42543304a4f", - "created_date": "2024-08-16 23:58:37.183000", - "last_modified_date": "2024-08-16 23:58:37.183000", - "version": 0, - "name": "San Diego Chargers", - "short_name": "Chargers", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "76968239-d7b7-4ff6-92aa-4587ff5511c6", - "created_date": "2024-08-16 23:58:37.247000", - "last_modified_date": "2024-08-16 23:58:37.247000", - "version": 0, - "name": "Arizona Diamondbacks", - "short_name": "Diamondbacks", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "7aa7ad8f-addf-4d5c-bb4d-d17dcb0ed421", - "created_date": "2024-08-16 23:58:37.287000", - "last_modified_date": "2024-08-16 23:58:37.287000", - "version": 0, - "name": "Minnesota Timberwolves", - "short_name": "Timberwolves", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "7b8f738e-0d8b-48c9-9f0a-033a05c339c2", - "created_date": "2024-08-16 23:58:37.250000", - "last_modified_date": "2024-08-16 23:58:37.250000", - "version": 0, - "name": "Los Angeles Dodgers", - "short_name": "Dodgers", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "7c7f779a-3ee4-430d-9881-471bd0038ed8", - "created_date": "2024-08-16 23:58:37.318000", - "last_modified_date": "2024-08-16 23:58:37.318000", - "version": 0, - "name": "Atlanta Trashers", - "short_name": "Trashers", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "7cb06db5-ab24-4339-be69-8a43a12d3a9d", - "created_date": "2024-08-16 23:58:37.205000", - "last_modified_date": "2024-08-16 23:58:37.205000", - "version": 0, - "name": "San Francisco 49ers", - "short_name": "49ers", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "7dbc78d3-b86a-4c99-af7f-4ebc9fb5ded8", - "created_date": "2024-08-16 23:58:37.249000", - "last_modified_date": "2024-08-16 23:58:37.249000", - "version": 0, - "name": "Colorado Rockies", - "short_name": "Rockies", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "83aa94d5-ec8d-477f-8ce2-b6c4c3bac2e5", - "created_date": "2024-08-16 23:58:37.180000", - "last_modified_date": "2024-08-16 23:58:37.180000", - "version": 0, - "name": "Kansas City Chiefs", - "short_name": "Chiefs", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "88301bde-3585-4d3d-830b-b4bd42b68dd5", - "created_date": "2024-08-16 23:58:37.208000", - "last_modified_date": "2024-08-16 23:58:37.208000", - "version": 0, - "name": "Houston Oilers", - "short_name": "Oilers", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "889741ce-2395-474b-a4eb-0a84495a3143", - "created_date": "2024-08-16 23:58:37.260000", - "last_modified_date": "2024-08-16 23:58:37.260000", - "version": 0, - "name": "New Jersey Nets", - "short_name": "Mets", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "88fb6005-0ec2-4e5e-8f78-8c2c172c0ce5", - "created_date": "2024-08-16 23:58:37.162000", - "last_modified_date": "2024-08-16 23:58:37.162000", - "version": 0, - "name": "Indianapolis Colts", - "short_name": "Colts", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "8bde8fc7-5f66-4c92-a00f-c5d2bcc67101", - "created_date": "2024-08-16 23:58:37.184000", - "last_modified_date": "2024-08-16 23:58:37.184000", - "version": 0, - "name": "Seattle Seahawks", - "short_name": "Seahawks", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "8d1ce4d1-58e7-45a5-b5bb-63b9d7f59842", - "created_date": "2024-08-16 23:58:37.233000", - "last_modified_date": "2024-08-16 23:58:37.233000", - "version": 0, - "name": "Montreal Expos", - "short_name": "Expos", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "8fd8e69b-eeb6-48d2-8043-812c01f1ec46", - "created_date": "2024-08-16 23:58:37.200000", - "last_modified_date": "2024-08-16 23:58:37.200000", - "version": 0, - "name": "Atlanta Falcons", - "short_name": "Falcons", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "934264a3-0333-44f3-a64a-8cb4b9a8e81e", - "created_date": "2024-08-16 23:58:37.166000", - "last_modified_date": "2024-08-16 23:58:37.166000", - "version": 0, - "name": "New England Patriots", - "short_name": "Patriots", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "93a667b8-85f4-45ae-a867-6df1d80e004f", - "created_date": "2024-08-16 23:58:37.314000", - "last_modified_date": "2024-08-16 23:58:37.314000", - "version": 0, - "name": "Philadelphia Flyers", - "short_name": "Flyers", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "963fb8ef-fb8f-4864-a249-1e00667c1915", - "created_date": "2024-08-16 23:58:37.286000", - "last_modified_date": "2024-08-16 23:58:37.286000", - "version": 0, - "name": "Houston Rockets", - "short_name": "Rockets", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "98f545c2-666b-449b-a072-e6146988cc19", - "created_date": "2024-08-16 23:58:37.306000", - "last_modified_date": "2024-08-16 23:58:37.306000", - "version": 0, - "name": "Montreal Canadiens", - "short_name": "Canadiens", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "99e0e365-347b-45cd-abf5-b37fbaffec8c", - "created_date": "2024-08-16 23:58:37.270000", - "last_modified_date": "2024-08-16 23:58:37.270000", - "version": 0, - "name": "Charlotte Hornets", - "short_name": "Hornets", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "9d318450-d458-490a-af79-68a4f8b001d4", - "created_date": "2024-08-16 23:58:37.296000", - "last_modified_date": "2024-08-16 23:58:37.296000", - "version": 0, - "name": "Los Angeles Lakers", - "short_name": "Lakers", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "9f2a4916-6ee3-4a44-addf-8de195b2a4ec", - "created_date": "2024-08-16 23:58:37.178000", - "last_modified_date": "2024-08-16 23:58:37.178000", - "version": 0, - "name": "Denver Broncos", - "short_name": "Broncos", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "a0128d73-e681-4b81-b7c3-64ee3fb538ac", - "created_date": "2024-08-16 23:58:37.220000", - "last_modified_date": "2024-08-16 23:58:37.220000", - "version": 0, - "name": "Detroit Tigers", - "short_name": "Tigers", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "a1740ba6-a74b-4481-990c-64af54375f15", - "created_date": "2024-08-16 23:58:37.230000", - "last_modified_date": "2024-08-16 23:58:37.230000", - "version": 0, - "name": "Atlanta Braves", - "short_name": "Braves", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "a188c637-a749-401f-9a93-20c96ce97d5d", - "created_date": "2024-08-16 23:58:37.297000", - "last_modified_date": "2024-08-16 23:58:37.297000", - "version": 0, - "name": "Phoenix Suns", - "short_name": "Suns", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "a4cc7d68-e9a4-465f-a877-ec69d351af62", - "created_date": "2024-08-16 23:58:37.339000", - "last_modified_date": "2024-08-16 23:58:37.339000", - "version": 0, - "name": "Vancouver Canucks", - "short_name": "Canucks", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "a9efdb53-2eb0-4e9a-8636-2da0cf0e595e", - "created_date": "2024-08-16 23:58:37.350000", - "last_modified_date": "2024-08-16 23:58:37.350000", - "version": 0, - "name": "San Jose Sharks", - "short_name": "Sharks", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "aaa8acb3-5d8a-4ff3-8774-9b691ca7984d", - "created_date": "2024-08-16 23:58:37.276000", - "last_modified_date": "2024-08-16 23:58:37.276000", - "version": 0, - "name": "Detroit Pistons", - "short_name": "Pistons", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "aae89396-6492-4e78-aa8d-d4e0f0f13b87", - "created_date": "2024-08-16 23:58:37.222000", - "last_modified_date": "2024-08-16 23:58:37.222000", - "version": 0, - "name": "Kansas City Royals", - "short_name": "Royals", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "ad9c54d8-edad-4620-828d-1f3a66825ef2", - "created_date": "2024-08-16 23:58:37.307000", - "last_modified_date": "2024-08-16 23:58:37.307000", - "version": 0, - "name": "Ottawa Senators", - "short_name": "Senators", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "b18050dd-a649-443d-b477-b8fe09cf8115", - "created_date": "2024-08-16 23:58:37.288000", - "last_modified_date": "2024-08-16 23:58:37.288000", - "version": 0, - "name": "San Antonio Spurs", - "short_name": "Spurs", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "b278553a-5a14-47cc-9de0-2a3a52487ac1", - "created_date": "2024-08-16 23:58:37.347000", - "last_modified_date": "2024-08-16 23:58:37.347000", - "version": 0, - "name": "Los Angeles Kings", - "short_name": "Kings", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "b336f3a1-be58-4962-8f6d-e035aa6ad423", - "created_date": "2024-08-16 23:58:37.238000", - "last_modified_date": "2024-08-16 23:58:37.238000", - "version": 0, - "name": "Cincinnati Reds", - "short_name": "Reds", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "b5fbcfbe-d644-41c6-a3fa-12b767a8fbe4", - "created_date": "2024-08-16 23:58:37.252000", - "last_modified_date": "2024-08-16 23:58:37.252000", - "version": 0, - "name": "San Diego Padres", - "short_name": "Padres", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "be316896-fd2d-4142-afb1-91c95a2d4268", - "created_date": "2024-08-16 23:58:37.298000", - "last_modified_date": "2024-08-16 23:58:37.298000", - "version": 0, - "name": "Portland Trail Blazers", - "short_name": "Blazers", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "becec8c2-aeeb-4d4a-9f8a-cae3a5ae14bd", - "created_date": "2024-08-16 23:58:37.237000", - "last_modified_date": "2024-08-16 23:58:37.237000", - "version": 0, - "name": "Chicago Cubs", - "short_name": "Cubs", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "bedd00af-d0d4-4b78-a0e7-f23c7b90bcb4", - "created_date": "2024-08-16 23:58:37.226000", - "last_modified_date": "2024-08-16 23:58:37.226000", - "version": 0, - "name": "Oakland Athletics", - "short_name": "Athletics", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "beebcc31-029d-488f-9748-91dd860d5d26", - "created_date": "2024-08-16 23:58:37.258000", - "last_modified_date": "2024-08-16 23:58:37.258000", - "version": 0, - "name": "Miami Heat", - "short_name": "Heat", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "bf9ab756-966f-4865-80f1-f5b1458dce51", - "created_date": "2024-08-16 23:58:37.325000", - "last_modified_date": "2024-08-16 23:58:37.325000", - "version": 0, - "name": "Washington Capitals", - "short_name": "Capitals", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "c0ae2c8a-46f2-4b9a-9319-6d3ba9a8e240", - "created_date": "2024-08-16 23:58:37.216000", - "last_modified_date": "2024-08-16 23:58:37.216000", - "version": 0, - "name": "Toronto Blue Jays", - "short_name": "Blue Jays", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "c12d24e5-01c3-4fbc-9594-d726ff4daa75", - "created_date": "2024-08-16 23:58:37.240000", - "last_modified_date": "2024-08-16 23:58:37.240000", - "version": 0, - "name": "Houston Astros", - "short_name": "Astros", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "c58f6aa5-3966-47a4-b9d5-f102854ee9da", - "created_date": "2024-08-16 23:58:37.242000", - "last_modified_date": "2024-08-16 23:58:37.242000", - "version": 0, - "name": "Milwaukee Brewers", - "short_name": "Brewers", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "c724eab7-d988-4c19-b973-425d65280514", - "created_date": "2024-08-16 23:58:37.337000", - "last_modified_date": "2024-08-16 23:58:37.337000", - "version": 0, - "name": "Minnesota Wild", - "short_name": "Wild", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "c80f077a-638b-4026-b956-9af57eed0541", - "created_date": "2024-08-16 23:58:37.313000", - "last_modified_date": "2024-08-16 23:58:37.313000", - "version": 0, - "name": "New York Rangers", - "short_name": "Rangers", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "d09dacce-a588-41bc-9c34-ca26755fcc7f", - "created_date": "2024-08-16 23:58:37.168000", - "last_modified_date": "2024-08-16 23:58:37.168000", - "version": 0, - "name": "New York Jets", - "short_name": "Jets", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "d2ca6ee6-a621-4516-80c2-59c2dd497554", - "created_date": "2024-08-16 23:58:37.207000", - "last_modified_date": "2024-08-16 23:58:37.207000", - "version": 0, - "name": "Houston Texans", - "short_name": "Texans", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "d2fdf923-086d-48bc-89e0-785e2c678b29", - "created_date": "2024-08-16 23:58:37.294000", - "last_modified_date": "2024-08-16 23:58:37.294000", - "version": 0, - "name": "Los Angeles Clippers", - "short_name": "Clippers", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "d98cc818-debe-427a-9bf1-20c3634b41d5", - "created_date": "2024-08-16 23:58:37.336000", - "last_modified_date": "2024-08-16 23:58:37.336000", - "version": 0, - "name": "Edmonton Oilers", - "short_name": "Oilers", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "dc14ff8b-6557-4d10-900d-56d5fb391389", - "created_date": "2024-08-16 23:58:37.246000", - "last_modified_date": "2024-08-16 23:58:37.246000", - "version": 0, - "name": "St.Louis Cardinals", - "short_name": "Cardinals", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "dd161125-04c7-4caa-ab0d-7d33007590b6", - "created_date": "2024-08-16 23:58:37.332000", - "last_modified_date": "2024-08-16 23:58:37.332000", - "version": 0, - "name": "St.Louis Blues", - "short_name": "Blues", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "e01349a3-56a5-4976-b66b-fca025deb3af", - "created_date": "2024-08-16 23:58:37.329000", - "last_modified_date": "2024-08-16 23:58:37.329000", - "version": 0, - "name": "Detroit Red Wings", - "short_name": "Red Wings", - "sport_id": "1b2ce3a2-52e5-43fd-b4b8-4185dae38b1e" - }, - { - "id": "e02a4afe-b89d-4d4d-bb25-0c850f856841", - "created_date": "2024-08-16 23:58:37.224000", - "last_modified_date": "2024-08-16 23:58:37.224000", - "version": 0, - "name": "Anaheim Angels", - "short_name": "Angels", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "e355bf76-ec12-4804-b4d3-88c18542651c", - "created_date": "2024-08-16 23:58:37.198000", - "last_modified_date": "2024-08-16 23:58:37.198000", - "version": 0, - "name": "Tampa Bay Buccaneers", - "short_name": "Buccaneers", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "e644821c-2fd9-46fa-b5e5-b6fb50e1ac08", - "created_date": "2024-08-16 23:58:37.232000", - "last_modified_date": "2024-08-16 23:58:37.232000", - "version": 0, - "name": "Florida Marlins", - "short_name": "Marlins", - "sport_id": "0718122d-8eea-4710-99cf-33a1f0a9c073" - }, - { - "id": "e8cd9040-08de-4b5a-a631-5637ff10e093", - "created_date": "2024-08-16 23:58:37.282000", - "last_modified_date": "2024-08-16 23:58:37.282000", - "version": 0, - "name": "Dallas Mavericks", - "short_name": "Mavericks", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "ee83a64c-f2fa-4259-8876-8f6926d51879", - "created_date": "2024-08-16 23:58:37.193000", - "last_modified_date": "2024-08-16 23:58:37.193000", - "version": 0, - "name": "Chicago Bears", - "short_name": "Bears", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "f1b524e1-15df-4b69-9fe4-9b507734be74", - "created_date": "2024-08-16 23:58:37.291000", - "last_modified_date": "2024-08-16 23:58:37.291000", - "version": 0, - "name": "Vancouver Grizzlies", - "short_name": "Grizzlies", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "f6f36160-96ce-44bf-9021-ab8757d5b7d9", - "created_date": "2024-08-16 23:58:37.300000", - "last_modified_date": "2024-08-16 23:58:37.300000", - "version": 0, - "name": "Sacramento Kings", - "short_name": "Kings", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - }, - { - "id": "fa930af3-e772-49c6-90d0-901d9778faa8", - "created_date": "2024-08-16 23:58:37.182000", - "last_modified_date": "2024-08-16 23:58:37.182000", - "version": 0, - "name": "Oakland Raiders", - "short_name": "Raiders", - "sport_id": "d28aec97-5c54-4f28-955c-7b9e725e5fe6" - }, - { - "id": "fd85a9b2-5ef3-4eb1-8370-37c286974971", - "created_date": "2024-08-16 23:58:37.301000", - "last_modified_date": "2024-08-16 23:58:37.301000", - "version": 0, - "name": "Seattle SuperSonics", - "short_name": "SuperSonics", - "sport_id": "d6deeeb1-6ad2-4fbc-9b8e-0035541edadb" - } - ], - "comic_work": [ - { - "id": "3aeb71e9-0a0b-43d0-bdfe-2244f0e6ce64", - "created_date": "2024-08-16 23:58:35.423000", - "last_modified_date": "2024-08-16 23:58:35.423000", - "version": 0, - "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", - "comic_id": "e54ebebe-701a-4d60-88ce-4df9b34da6ca", - "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" - }, - { - "id": "41f678ef-7da7-496e-8a5f-d197bddd06c5", - "created_date": "2024-08-16 23:58:35.419000", - "last_modified_date": "2024-08-16 23:58:35.419000", - "version": 0, - "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", - "comic_id": "b6383b7f-1c86-42d9-a3f6-d2a4bc96dc51", - "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" - }, - { - "id": "5412b81f-66e3-4539-82a6-cf9bc2665367", - "created_date": "2024-08-16 23:58:35.416000", - "last_modified_date": "2024-08-16 23:58:35.416000", - "version": 0, - "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", - "comic_id": "34baf0ec-2af4-4ceb-9f54-ab92fb9a0b96", - "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" - }, - { - "id": "56ce28ea-48e9-454a-872d-f1542206face", - "created_date": "2024-08-16 23:58:35.430000", - "last_modified_date": "2024-08-16 23:58:35.430000", - "version": 0, - "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", - "comic_id": "46e41bf6-1c75-4e2b-9905-9dfda3bcfa9c", - "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" - }, - { - "id": "5cb922e2-7304-4f55-a3cb-9f9db34bffbc", - "created_date": "2024-08-16 23:58:35.438000", - "last_modified_date": "2024-08-16 23:58:35.438000", - "version": 0, - "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", - "comic_id": "dea3ab08-5e3e-4438-b28f-fcb711a3b593", - "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" - }, - { - "id": "65343fb2-2217-4815-8352-fff6dab1eab6", - "created_date": "2024-08-16 23:58:35.421000", - "last_modified_date": "2024-08-16 23:58:35.421000", - "version": 0, - "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", - "comic_id": "d37740d1-9c0d-480f-bf14-16f868f50a2c", - "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" - }, - { - "id": "75321376-d97d-4c73-be91-157f3a0bc2e1", - "created_date": "2024-08-16 23:58:35.452000", - "last_modified_date": "2024-08-16 23:58:35.452000", - "version": 0, - "artist_id": "b81c0868-a38c-4a2c-a64e-92a17c88fa72", - "comic_id": "b6383b7f-1c86-42d9-a3f6-d2a4bc96dc51", - "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" - }, - { - "id": "9df312f5-fe0c-4379-976c-b864c1df1efd", - "created_date": "2024-08-16 23:58:35.435000", - "last_modified_date": "2024-08-16 23:58:35.435000", - "version": 0, - "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", - "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", - "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" - }, - { - "id": "b2f40e1f-4c4d-4f42-b470-9c11bdce75e2", - "created_date": "2024-08-16 23:58:35.414000", - "last_modified_date": "2024-08-16 23:58:35.414000", - "version": 0, - "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", - "comic_id": "5aa143a9-d0a1-457f-b178-c8c71951dd91", - "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" - }, - { - "id": "b36a59f1-e82f-426d-916b-05e8e7a2893a", - "created_date": "2024-08-16 23:58:35.412000", - "last_modified_date": "2024-08-16 23:58:35.412000", - "version": 0, - "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", - "comic_id": "03c5b145-69d4-4d7e-8323-cb2f81060829", - "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" - }, - { - "id": "b66f93bf-0e33-4837-bc1c-9176bcee32e9", - "created_date": "2024-08-16 23:58:35.428000", - "last_modified_date": "2024-08-16 23:58:35.428000", - "version": 0, - "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", - "comic_id": "8ad36a79-9436-455d-8096-0f1b73c22f13", - "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" - }, - { - "id": "b9015fe2-25f0-4a13-a476-98d8065d2167", - "created_date": "2024-08-16 23:58:35.446000", - "last_modified_date": "2024-08-16 23:58:35.446000", - "version": 0, - "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", - "comic_id": "becf411e-8fe2-470e-8bde-7991c59988e0", - "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" - }, - { - "id": "bba7fe64-6f8a-4cfa-acdf-9430be20a5dc", - "created_date": "2024-08-16 23:58:35.444000", - "last_modified_date": "2024-08-16 23:58:35.444000", - "version": 0, - "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", - "comic_id": "1b7de491-6bbb-404e-a2e5-a20a123e3fe5", - "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" - }, - { - "id": "c0bcd7d4-2afd-41e4-afba-52eaee345f02", - "created_date": "2024-08-16 23:58:35.425000", - "last_modified_date": "2024-08-16 23:58:35.425000", - "version": 0, - "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", - "comic_id": "fd313197-70f1-45d5-8ca3-c8b6d828254e", - "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" - }, - { - "id": "c7a46f94-1f47-45c9-ad03-ea69518023f7", - "created_date": "2024-08-16 23:58:35.407000", - "last_modified_date": "2024-08-16 23:58:35.407000", - "version": 0, - "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", - "comic_id": "54ce47f2-d611-4d22-9ef5-c57e6d3e5967", - "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" - }, - { - "id": "d403a597-efb1-4b6b-93ba-432aeb6464f7", - "created_date": "2024-08-16 23:58:35.450000", - "last_modified_date": "2024-08-16 23:58:35.450000", - "version": 0, - "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", - "comic_id": "e6b93088-b350-412b-9634-227216ff252a", - "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" - }, - { - "id": "dffde7c9-fb4b-4d8c-a568-51fb4d0e33be", - "created_date": "2024-08-16 23:58:35.433000", - "last_modified_date": "2024-08-16 23:58:35.433000", - "version": 0, - "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", - "comic_id": "abfcb11c-7757-4db8-879e-b0d1803819d9", - "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" - }, - { - "id": "f7d36434-f17b-494c-ab13-0cf3fffef6f3", - "created_date": "2024-08-16 23:58:35.441000", - "last_modified_date": "2024-08-16 23:58:35.441000", - "version": 0, - "artist_id": "b457d79a-3a04-41fa-b135-f6f86af575af", - "comic_id": "240a8a5d-eb07-4ce1-9fdd-a1b888c7426c", - "work_type_id": "8bca067a-7ba7-4f94-b317-c521b315bee8" - } - ], - "worktype": [ - { - "id": "6e454a13-672e-40fc-98ec-82e156fbf2e7", - "created_date": "2024-08-16 23:58:34.802000", - "last_modified_date": "2024-08-16 23:58:34.802000", - "version": 0, - "name": "Penciler" - }, - { - "id": "8bca067a-7ba7-4f94-b317-c521b315bee8", - "created_date": "2024-08-16 23:58:34.797000", - "last_modified_date": "2024-08-16 23:58:34.797000", - "version": 0, - "name": "Writer" - }, - { - "id": "d506f76b-ce8d-4789-9716-64cd0c10ebc6", - "created_date": "2024-08-16 23:58:34.807000", - "last_modified_date": "2024-08-16 23:58:34.807000", - "version": 0, - "name": "Inker" - } - ], - "artist": [ - { - "id": "7d1e0a4c-eb44-4208-962c-7c069a81c3ac", - "created_date": "2024-08-16 23:58:34.787000", - "last_modified_date": "2024-08-16 23:58:34.787000", - "version": 0, - "name": "Whedon, Joss" - }, - { - "id": "b457d79a-3a04-41fa-b135-f6f86af575af", - "created_date": "2024-08-16 23:58:34.780000", - "last_modified_date": "2024-08-16 23:58:34.780000", - "version": 0, - "name": "Turner, Michael" - }, - { - "id": "b7d3316c-5368-4554-9857-a1b96231d751", - "created_date": "2024-08-16 23:58:34.784000", - "last_modified_date": "2024-08-16 23:58:34.784000", - "version": 0, - "name": "Marz, Ron" - }, - { - "id": "b81c0868-a38c-4a2c-a64e-92a17c88fa72", - "created_date": "2024-08-16 23:58:34.793000", - "last_modified_date": "2024-08-16 23:58:34.793000", - "version": 0, - "name": "Bendis, Brian Michael" - }, - { - "id": "d5a3a6de-825f-46f5-90d4-df6482ba9768", - "created_date": "2024-08-16 23:58:34.790000", - "last_modified_date": "2024-08-16 23:58:34.790000", - "version": 0, - "name": "Land, Greg" - } - ], - "module_data": [ - { - "id": "2589c9b6-45ae-4ef7-9fd4-de8a903c4576", - "created_date": "2024-08-16 23:58:37.138000", - "last_modified_date": "2024-08-16 23:58:37.138000", - "version": 0, - "module_name": "Comics", - "import_data": 0 - }, - { - "id": "78e458f5-3d26-41b0-871c-9a342613f6fb", - "created_date": "2024-08-16 23:58:37.143000", - "last_modified_date": "2024-08-16 23:58:37.143000", - "version": 0, - "module_name": "Media", - "import_data": 0 - }, - { - "id": "d43897dc-6ea9-4777-ba62-41e947a30983", - "created_date": "2024-08-16 23:58:34.698000", - "last_modified_date": "2024-08-16 23:58:34.698000", - "version": 0, - "module_name": "Bookshelf", - "import_data": 0 - }, - { - "id": "dd46b95f-e2cb-439c-b4d7-a109fec94cd7", - "created_date": "2024-08-16 23:58:37.607000", - "last_modified_date": "2024-08-16 23:58:37.607000", - "version": 0, - "module_name": "TradeYourSportsCards", - "import_data": 0 - } - ], - "bookshelf_publisher": [], - "player": [ - { - "id": "0166756b-e546-42fa-bdc9-dca8bd88278d", - "created_date": "2024-08-16 23:58:37.540000", - "last_modified_date": "2024-08-16 23:58:37.540000", - "version": 0, - "first_name": "Antowain", - "last_name": "Smith" - }, - { - "id": "0204701a-8421-418c-bfa4-90c5de616108", - "created_date": "2024-08-16 23:58:37.517000", - "last_modified_date": "2024-08-16 23:58:37.517000", - "version": 0, - "first_name": "Troy", - "last_name": "Aikman" - }, - { - "id": "02fc65f6-7d12-41c1-936d-e5dd20da58ba", - "created_date": "2024-08-16 23:58:37.504000", - "last_modified_date": "2024-08-16 23:58:37.504000", - "version": 0, - "first_name": "Jerome", - "last_name": "Bettis" - }, - { - "id": "0fa94659-7cfe-41ac-abec-ed00462f60dd", - "created_date": "2024-08-16 23:58:37.536000", - "last_modified_date": "2024-08-16 23:58:37.536000", - "version": 0, - "first_name": "Charlie", - "last_name": "Garner" - }, - { - "id": "12bd126b-3c53-493f-a2d6-da68f6f212ea", - "created_date": "2024-08-16 23:58:37.508000", - "last_modified_date": "2024-08-16 23:58:37.508000", - "version": 0, - "first_name": "Kevin", - "last_name": "Lockett" - }, - { - "id": "1560dc39-f3f9-43e1-82d9-a9fb0dab0793", - "created_date": "2024-08-16 23:58:37.537000", - "last_modified_date": "2024-08-16 23:58:37.537000", - "version": 0, - "first_name": "Drew", - "last_name": "Bledsoe" - }, - { - "id": "23710a7d-27f6-4017-8377-d4eec2ab9f45", - "created_date": "2024-08-16 23:58:37.525000", - "last_modified_date": "2024-08-16 23:58:37.525000", - "version": 0, - "first_name": "Torrance", - "last_name": "Small" - }, - { - "id": "24634014-f395-4409-aeb3-fa0af013b302", - "created_date": "2024-08-16 23:58:37.541000", - "last_modified_date": "2024-08-16 23:58:37.541000", - "version": 0, - "first_name": "Terry", - "last_name": "Glenn" - }, - { - "id": "2547e730-6a1d-418d-98c1-4d1c09c9f021", - "created_date": "2024-08-16 23:58:37.511000", - "last_modified_date": "2024-08-16 23:58:37.511000", - "version": 0, - "first_name": "James", - "last_name": "Jett" - }, - { - "id": "2c2b9866-eae7-43b3-b0af-6a56a4dec2d6", - "created_date": "2024-08-16 23:58:37.530000", - "last_modified_date": "2024-08-16 23:58:37.530000", - "version": 0, - "first_name": "Chris", - "last_name": "Chandler" - }, - { - "id": "4172d642-5b3d-4c60-8987-e3d37aee1822", - "created_date": "2024-08-16 23:58:37.507000", - "last_modified_date": "2024-08-16 23:58:37.507000", - "version": 0, - "first_name": "Warren", - "last_name": "Moon" - }, - { - "id": "4216df43-16a4-4e94-a2c8-324f5785e2b9", - "created_date": "2024-08-16 23:58:37.542000", - "last_modified_date": "2024-08-16 23:58:37.542000", - "version": 0, - "first_name": "Jerry", - "last_name": "Rice" - }, - { - "id": "48afc549-12a8-46ce-a5d2-c01d8dca4492", - "created_date": "2024-08-16 23:58:37.535000", - "last_modified_date": "2024-08-16 23:58:37.535000", - "version": 0, - "first_name": "Tai", - "last_name": "Streets" - }, - { - "id": "4f6c6a79-9512-4927-8be1-3c74405870e1", - "created_date": "2024-08-16 23:58:37.528000", - "last_modified_date": "2024-08-16 23:58:37.528000", - "version": 0, - "first_name": "Adrian", - "last_name": "Murrell" - }, - { - "id": "5038dd4f-203f-4cb7-99dd-c12c7da4735f", - "created_date": "2024-08-16 23:58:37.498000", - "last_modified_date": "2024-08-16 23:58:37.498000", - "version": 0, - "first_name": "Jamal", - "last_name": "Lewis" - }, - { - "id": "5ce64865-db01-4c55-9bb1-72ea1bea9b08", - "created_date": "2024-08-16 23:58:37.499000", - "last_modified_date": "2024-08-16 23:58:37.499000", - "version": 0, - "first_name": "Jermaine", - "last_name": "Lewis" - }, - { - "id": "5ee26469-7618-45ef-ba98-f83c3931f2d3", - "created_date": "2024-08-16 23:58:37.495000", - "last_modified_date": "2024-08-16 23:58:37.495000", - "version": 0, - "first_name": "Tim", - "last_name": "Couch" - }, - { - "id": "61baa0c9-4766-4f95-b3b5-dbb2eee80971", - "created_date": "2024-08-16 23:58:37.522000", - "last_modified_date": "2024-08-16 23:58:37.522000", - "version": 0, - "first_name": "Ron", - "last_name": "Dayne" - }, - { - "id": "6be96b96-6d3e-4106-8078-3b5d8c2d79da", - "created_date": "2024-08-16 23:58:37.532000", - "last_modified_date": "2024-08-16 23:58:37.532000", - "version": 0, - "first_name": "Ricky", - "last_name": "Williams" - }, - { - "id": "7b76a40c-c01c-4050-b4fb-c9da33fa1bdd", - "created_date": "2024-08-16 23:58:37.520000", - "last_modified_date": "2024-08-16 23:58:37.520000", - "version": 0, - "first_name": "Chris", - "last_name": "Brazzell" - }, - { - "id": "80ad0a60-a164-4d6c-9d94-155eaafd71f1", - "created_date": "2024-08-16 23:58:37.527000", - "last_modified_date": "2024-08-16 23:58:37.527000", - "version": 0, - "first_name": "Chad", - "last_name": "Lewis" - }, - { - "id": "87ddf101-82b7-4c17-9789-945c1696c1a3", - "created_date": "2024-08-16 23:58:37.531000", - "last_modified_date": "2024-08-16 23:58:37.531000", - "version": 0, - "first_name": "Danny", - "last_name": "Kanell" - }, - { - "id": "887504f8-5038-4335-90ba-487ac139155c", - "created_date": "2024-08-16 23:58:37.497000", - "last_modified_date": "2024-08-16 23:58:37.497000", - "version": 0, - "first_name": "Aaron", - "last_name": "Shea" - }, - { - "id": "8b8c84d1-244f-4d2b-a1e4-b3ff3f57cadd", - "created_date": "2024-08-16 23:58:37.523000", - "last_modified_date": "2024-08-16 23:58:37.523000", - "version": 0, - "first_name": "Na", - "last_name": "Brown" - }, - { - "id": "91a1a8b2-5dc8-4cbe-857e-25fd53ffc44e", - "created_date": "2024-08-16 23:58:37.512000", - "last_modified_date": "2024-08-16 23:58:37.512000", - "version": 0, - "first_name": "Mack", - "last_name": "Strong" - }, - { - "id": "983cc139-f2a2-46cd-9fdc-3ec1abf0d5a5", - "created_date": "2024-08-16 23:58:37.505000", - "last_modified_date": "2024-08-16 23:58:37.505000", - "version": 0, - "first_name": "Kordell", - "last_name": "Stewart" - }, - { - "id": "9cfea3d3-dc20-4cb0-a7c5-b12abb7f2520", - "created_date": "2024-08-16 23:58:37.515000", - "last_modified_date": "2024-08-16 23:58:37.515000", - "version": 0, - "first_name": "Brock", - "last_name": "Huard" - }, - { - "id": "a189b76f-9390-49f1-b5ab-47b35d00cb7f", - "created_date": "2024-08-16 23:58:37.493000", - "last_modified_date": "2024-08-16 23:58:37.493000", - "version": 0, - "first_name": "Tedy", - "last_name": "Bruschi" - }, - { - "id": "ac6b69ff-970a-4ead-bc5e-73dbcb5a4040", - "created_date": "2024-08-16 23:58:37.533000", - "last_modified_date": "2024-08-16 23:58:37.533000", - "version": 0, - "first_name": "Jeff", - "last_name": "Garcia" - }, - { - "id": "c9573462-268d-4138-9a33-1551e4e61f26", - "created_date": "2024-08-16 23:58:37.509000", - "last_modified_date": "2024-08-16 23:58:37.509000", - "version": 0, - "first_name": "Rich", - "last_name": "Gannon" - }, - { - "id": "cd4e3f5f-1e03-4d9b-973a-e8f9b7892dd8", - "created_date": "2024-08-16 23:58:37.503000", - "last_modified_date": "2024-08-16 23:58:37.503000", - "version": 0, - "first_name": "Chris", - "last_name": "Fuamatu-Ma'afala" - }, - { - "id": "d6b1a09d-3984-41cb-8b0a-d37d77e2c8ff", - "created_date": "2024-08-16 23:58:37.490000", - "last_modified_date": "2024-08-16 23:58:37.490000", - "version": 0, - "first_name": "Jerome", - "last_name": "Pathon" - }, - { - "id": "d8b7a3fd-8944-4a0a-b1d9-d42a33314f24", - "created_date": "2024-08-16 23:58:37.544000", - "last_modified_date": "2024-08-16 23:58:37.544000", - "version": 0, - "first_name": "Terrell", - "last_name": "Owens" - }, - { - "id": "de1e8d59-c2e2-4f68-a72a-14b4322fdf88", - "created_date": "2024-08-16 23:58:37.545000", - "last_modified_date": "2024-08-16 23:58:37.545000", - "version": 0, - "first_name": "Isaac", - "last_name": "Bruce" - }, - { - "id": "e3cb8252-3112-4d34-b20c-e586d900d17e", - "created_date": "2024-08-16 23:58:37.516000", - "last_modified_date": "2024-08-16 23:58:37.516000", - "version": 0, - "first_name": "Ricky", - "last_name": "Watters" - }, - { - "id": "e83cddec-19b0-4d35-929c-8acd7a7b7cbf", - "created_date": "2024-08-16 23:58:37.501000", - "last_modified_date": "2024-08-16 23:58:37.501000", - "version": 0, - "first_name": "Tony", - "last_name": "Banks" - }, - { - "id": "ec53c149-f00d-4130-8cf5-a65102a70ca2", - "created_date": "2024-08-16 23:58:37.546000", - "last_modified_date": "2024-08-16 23:58:37.546000", - "version": 0, - "first_name": "Trung", - "last_name": "Canidate" - }, - { - "id": "febb0d11-ac0f-49ea-b92d-14b44128eb37", - "created_date": "2024-08-16 23:58:37.519000", - "last_modified_date": "2024-08-16 23:58:37.519000", - "version": 0, - "first_name": "David", - "last_name": "LaFleur" - } - ], - "mail_account": [ - { - "id": "bfb66ce0-932d-413d-a996-923373a8ed49", - "created_date": "2024-08-16 23:58:34.684000", - "last_modified_date": "2024-08-16 23:58:34.684000", - "version": 0, - "host": "corky.svpdata.eu", - "port": 143, - "protocol": "imap", - "user_name": "thomas.peetz@thpeetz.de", - "password": "fS9f4JYDIO7A", - "start_tls": 1 - } - ], - "token": [], - "story_arc": [ - { - "id": "26a2f57e-a3f8-4ee4-8961-d49787af94c7", - "created_date": "2024-08-16 23:58:35.466000", - "last_modified_date": "2024-08-16 23:58:35.466000", - "version": 0, - "name": "Bloom", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb" - }, - { - "id": "6c4b5e81-d951-4f7b-9a56-a862f8150434", - "created_date": "2024-08-16 23:58:35.457000", - "last_modified_date": "2024-08-16 23:58:35.457000", - "version": 0, - "name": "Higher Learning", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb" - }, - { - "id": "fa78f350-91c4-4c57-9183-9293cdef6eec", - "created_date": "2024-08-16 23:58:35.461000", - "last_modified_date": "2024-08-16 23:58:35.461000", - "version": 0, - "name": "Mind Games", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb" - } - ], - "issue": [ - { - "id": "009dc046-a9e4-4bd6-8a8a-ffe34d54d8a1", - "created_date": "2024-08-16 23:58:36.128000", - "last_modified_date": "2024-08-16 23:58:36.128000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "f3231681-cd2b-4ff9-bfa2-5d8f631bee4d", - "volume_id": null - }, - { - "id": "01272372-57d4-4b94-8d95-1d09c5eb18d2", - "created_date": "2024-08-16 23:58:36.181000", - "last_modified_date": "2024-08-16 23:58:36.181000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", - "volume_id": null - }, - { - "id": "0138df17-570c-4072-9c8e-cf9cbf7b30e3", - "created_date": "2024-08-16 23:58:36.050000", - "last_modified_date": "2024-08-16 23:58:36.050000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", - "volume_id": null - }, - { - "id": "016d7c3a-522c-410e-a440-77e483f7f7d1", - "created_date": "2024-08-16 23:58:36.610000", - "last_modified_date": "2024-08-16 23:58:36.610000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "27", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "01bbd3dd-ef86-492c-8208-bb5c2aa5d984", - "created_date": "2024-08-16 23:58:36.131000", - "last_modified_date": "2024-08-16 23:58:36.131000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "c0543c9f-d712-4dce-9b59-2bf73b800b31", - "volume_id": null - }, - { - "id": "01c7e2a2-df2a-4244-8165-75458ce62401", - "created_date": "2024-08-16 23:58:36.026000", - "last_modified_date": "2024-08-16 23:58:36.026000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "16", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", - "volume_id": null - }, - { - "id": "01f83a75-adbe-485b-a726-16e4d1d5b691", - "created_date": "2024-08-16 23:58:36.943000", - "last_modified_date": "2024-08-16 23:58:36.943000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "43", - "comic_id": "2a4b287e-4b05-4016-8eb4-21fc225be24d", - "volume_id": null - }, - { - "id": "023a721d-1951-432c-96b6-c8e6bf5870fc", - "created_date": "2024-08-16 23:58:36.880000", - "last_modified_date": "2024-08-16 23:58:36.880000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", - "volume_id": null - }, - { - "id": "0283c944-0c5b-4a50-bf53-a11e6bdd1280", - "created_date": "2024-08-16 23:58:35.616000", - "last_modified_date": "2024-08-16 23:58:35.616000", - "version": 0, - "in_stock": 0, - "is_read": 1, - "issue_number": "3", - "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", - "volume_id": null - }, - { - "id": "02f6f28e-3672-4893-bc29-11fc3c637ff3", - "created_date": "2024-08-16 23:58:36.344000", - "last_modified_date": "2024-08-16 23:58:36.344000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "8", - "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", - "volume_id": null - }, - { - "id": "035fa8c9-2107-4bdf-90d7-b6fede18aeb1", - "created_date": "2024-08-16 23:58:36.871000", - "last_modified_date": "2024-08-16 23:58:36.871000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", - "volume_id": null - }, - { - "id": "03d219d7-0faf-4c2d-9b78-2113ccdfeca2", - "created_date": "2024-08-16 23:58:36.606000", - "last_modified_date": "2024-08-16 23:58:36.606000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "24", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "04069b6d-aac0-4ff0-bb22-68c6483f775e", - "created_date": "2024-08-16 23:58:37.100000", - "last_modified_date": "2024-08-16 23:58:37.100000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "07574acf-8f77-4da7-87fd-c49f40536baf", - "volume_id": null - }, - { - "id": "04384bcf-1806-402a-999d-356d512625f3", - "created_date": "2024-08-16 23:58:35.803000", - "last_modified_date": "2024-08-16 23:58:35.803000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "b51c738d-8ba1-4451-8f89-f49c1968ac42", - "volume_id": null - }, - { - "id": "04f41e26-37ac-4264-aa9c-8bbb46de41ff", - "created_date": "2024-08-16 23:58:36.264000", - "last_modified_date": "2024-08-16 23:58:36.264000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "5d0fd720-7875-4f9f-86eb-07ee0723908f", - "volume_id": null - }, - { - "id": "0600fbe7-a8d2-4373-8c44-b266d42eafa2", - "created_date": "2024-08-16 23:58:36.227000", - "last_modified_date": "2024-08-16 23:58:36.227000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "fe7fb34e-5ff2-4f6d-a649-0be19a368f46", - "volume_id": null - }, - { - "id": "06888bbc-1c0e-4a99-b976-e60d5b2b0dcc", - "created_date": "2024-08-16 23:58:35.819000", - "last_modified_date": "2024-08-16 23:58:35.819000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "89c5ea13-997a-4831-87cc-ee76ea05c71e", - "volume_id": null - }, - { - "id": "06e5bc77-7b77-4bda-adba-2f4d2781b89f", - "created_date": "2024-08-16 23:58:37.022000", - "last_modified_date": "2024-08-16 23:58:37.022000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "514", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "070d66c0-ac95-44e2-ab97-e0221308c027", - "created_date": "2024-08-16 23:58:36.494000", - "last_modified_date": "2024-08-16 23:58:36.494000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "8", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "072689aa-ebf2-4b1d-9283-0217c1e55ea8", - "created_date": "2024-08-16 23:58:36.117000", - "last_modified_date": "2024-08-16 23:58:36.117000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "17b50be7-aca7-446e-8729-3706f636d29d", - "volume_id": null - }, - { - "id": "076704a2-3263-4b71-a081-f5588a9fec1b", - "created_date": "2024-08-16 23:58:35.789000", - "last_modified_date": "2024-08-16 23:58:35.789000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "5a72a63d-8fbd-46a8-b201-9b6e035c782a", - "volume_id": null - }, - { - "id": "087d31b2-5c22-43f1-8fa5-fa712f5f5ba0", - "created_date": "2024-08-16 23:58:36.199000", - "last_modified_date": "2024-08-16 23:58:36.199000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", - "volume_id": null - }, - { - "id": "08c7d893-537d-4e4c-83ad-214e70dd2ab5", - "created_date": "2024-08-16 23:58:36.414000", - "last_modified_date": "2024-08-16 23:58:36.414000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "50", - "comic_id": "57965b27-1330-4921-8c0b-4b09ee06084f", - "volume_id": null - }, - { - "id": "08f61e9c-7c03-459e-b9d3-643f8e6f63d6", - "created_date": "2024-08-16 23:58:36.620000", - "last_modified_date": "2024-08-16 23:58:36.620000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "33", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "095efe2d-ce27-4614-a731-ea1aa760dce9", - "created_date": "2024-08-16 23:58:36.225000", - "last_modified_date": "2024-08-16 23:58:36.225000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "fe7fb34e-5ff2-4f6d-a649-0be19a368f46", - "volume_id": null - }, - { - "id": "09aa99f6-457d-47fd-81df-7aa729888075", - "created_date": "2024-08-16 23:58:37.004000", - "last_modified_date": "2024-08-16 23:58:37.004000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "851bc748-2602-4f20-826f-a59a7087d11f", - "volume_id": null - }, - { - "id": "09b4f24c-e6cf-4aa4-802e-4d21bed00b1f", - "created_date": "2024-08-16 23:58:36.205000", - "last_modified_date": "2024-08-16 23:58:36.205000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", - "volume_id": null - }, - { - "id": "0a347f5f-293a-4fd6-a927-7504fd815bc2", - "created_date": "2024-08-16 23:58:36.492000", - "last_modified_date": "2024-08-16 23:58:36.492000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "0a40ee06-8c79-47d0-97ff-28e42951f36b", - "created_date": "2024-08-16 23:58:36.876000", - "last_modified_date": "2024-08-16 23:58:36.876000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", - "volume_id": null - }, - { - "id": "0a4f93d3-bc12-40d5-ba6e-1c31f57cad6c", - "created_date": "2024-08-16 23:58:35.663000", - "last_modified_date": "2024-08-16 23:58:35.663000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", - "volume_id": null - }, - { - "id": "0a6310a0-8fec-4582-b5d9-d0cba3d1e9df", - "created_date": "2024-08-16 23:58:36.443000", - "last_modified_date": "2024-08-16 23:58:36.443000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "19", - "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", - "volume_id": null - }, - { - "id": "0a8180d1-b208-4168-b9a3-5d1f1c97a30e", - "created_date": "2024-08-16 23:58:36.855000", - "last_modified_date": "2024-08-16 23:58:36.855000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", - "volume_id": null - }, - { - "id": "0aa0918f-bbae-4709-a370-1fac7eb33993", - "created_date": "2024-08-16 23:58:35.793000", - "last_modified_date": "2024-08-16 23:58:35.793000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "b51c738d-8ba1-4451-8f89-f49c1968ac42", - "volume_id": null - }, - { - "id": "0b699566-3222-4109-b06a-d45f8b5ca07e", - "created_date": "2024-08-16 23:58:36.959000", - "last_modified_date": "2024-08-16 23:58:36.959000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "0b9dab2f-2fed-4a7e-909f-d83c483831cf", - "created_date": "2024-08-16 23:58:35.774000", - "last_modified_date": "2024-08-16 23:58:35.774000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "25", - "comic_id": "031b7570-04bc-4f56-834e-61c5792b6e5e", - "volume_id": null - }, - { - "id": "0ba141fd-4bae-4023-bdba-f6d4cfe6d762", - "created_date": "2024-08-16 23:58:36.119000", - "last_modified_date": "2024-08-16 23:58:36.119000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "17b50be7-aca7-446e-8729-3706f636d29d", - "volume_id": null - }, - { - "id": "0c8b459d-add1-456e-aa1a-e7edb6563f2b", - "created_date": "2024-08-16 23:58:36.633000", - "last_modified_date": "2024-08-16 23:58:36.633000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "0e6688d2-f347-4800-83b1-1f094b658084", - "volume_id": null - }, - { - "id": "0cee2e8f-726a-4099-8ed5-3414b45fe895", - "created_date": "2024-08-16 23:58:36.073000", - "last_modified_date": "2024-08-16 23:58:36.073000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "12", - "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", - "volume_id": null - }, - { - "id": "0d2076f6-2516-47bd-aa74-b614c557e4ad", - "created_date": "2024-08-16 23:58:36.673000", - "last_modified_date": "2024-08-16 23:58:36.673000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", - "volume_id": null - }, - { - "id": "0d2e7bc8-a6a6-4d1a-b333-85b6e8e404e4", - "created_date": "2024-08-16 23:58:36.569000", - "last_modified_date": "2024-08-16 23:58:36.569000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "45bd0c8a-845f-4ab3-9281-c31e5a3d4472", - "volume_id": null - }, - { - "id": "0d3c0bb1-4ac8-4e8b-b925-0c2408a6cd22", - "created_date": "2024-08-16 23:58:36.676000", - "last_modified_date": "2024-08-16 23:58:36.676000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", - "volume_id": null - }, - { - "id": "0d520d9f-c13a-40dc-ae9d-dfcd21e627b5", - "created_date": "2024-08-16 23:58:36.121000", - "last_modified_date": "2024-08-16 23:58:36.121000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "17b50be7-aca7-446e-8729-3706f636d29d", - "volume_id": null - }, - { - "id": "0dda9d31-2aac-4e9c-be85-948e3afe41ee", - "created_date": "2024-08-16 23:58:35.629000", - "last_modified_date": "2024-08-16 23:58:35.629000", - "version": 0, - "in_stock": 0, - "is_read": 1, - "issue_number": "7", - "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", - "volume_id": null - }, - { - "id": "0e0dcb02-ac79-4667-8fda-078e7a605c09", - "created_date": "2024-08-16 23:58:36.390000", - "last_modified_date": "2024-08-16 23:58:36.390000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "210", - "comic_id": "d24801e2-fbfe-4497-873f-4d8edb182ae4", - "volume_id": null - }, - { - "id": "0ed58cd0-ac3b-427d-bf6a-f06b35eeb975", - "created_date": "2024-08-16 23:58:36.409000", - "last_modified_date": "2024-08-16 23:58:36.409000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "fdcbf2b1-c3cb-44d8-888b-41260a87b0e4", - "volume_id": null - }, - { - "id": "0ed653c6-a0ca-45d2-88eb-7e1256dbb958", - "created_date": "2024-08-16 23:58:36.904000", - "last_modified_date": "2024-08-16 23:58:36.904000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "9aac6484-4c06-4286-815a-219aad25cc74", - "volume_id": null - }, - { - "id": "0ed7e9d6-ccbd-4dd1-82a0-31aee9c80404", - "created_date": "2024-08-16 23:58:35.840000", - "last_modified_date": "2024-08-16 23:58:35.840000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "0f49700a-e1c2-4ed1-9007-96778f77bbdc", - "created_date": "2024-08-16 23:58:36.840000", - "last_modified_date": "2024-08-16 23:58:36.840000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "9fcdd9a5-f1fb-4421-a352-9da8e2c12f81", - "volume_id": null - }, - { - "id": "0f6db6dd-6c53-4cd1-9a24-156928b41b13", - "created_date": "2024-08-16 23:58:36.046000", - "last_modified_date": "2024-08-16 23:58:36.046000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", - "volume_id": null - }, - { - "id": "0fbc698b-dfe5-4714-b367-6a9a47bd3ac6", - "created_date": "2024-08-16 23:58:36.534000", - "last_modified_date": "2024-08-16 23:58:36.534000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "2b2a07ba-c5a8-4cb8-b170-990cb941315a", - "volume_id": null - }, - { - "id": "10924942-003a-47b6-b801-ca99e7fdcbb2", - "created_date": "2024-08-16 23:58:37.078000", - "last_modified_date": "2024-08-16 23:58:37.078000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "231e65eb-eecb-4946-a643-6184b767e321", - "volume_id": null - }, - { - "id": "1135968b-ed0f-4510-9b73-df35fbaae6d8", - "created_date": "2024-08-16 23:58:35.968000", - "last_modified_date": "2024-08-16 23:58:35.968000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", - "volume_id": null - }, - { - "id": "1140b9c0-44b3-46d2-b427-10a8b908d95e", - "created_date": "2024-08-16 23:58:36.126000", - "last_modified_date": "2024-08-16 23:58:36.126000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "f3231681-cd2b-4ff9-bfa2-5d8f631bee4d", - "volume_id": null - }, - { - "id": "11dadbd4-22f1-465c-b1b4-5c6b3e1a988e", - "created_date": "2024-08-16 23:58:36.756000", - "last_modified_date": "2024-08-16 23:58:36.756000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", - "volume_id": null - }, - { - "id": "121e1529-443a-4518-a776-f53d02cedce9", - "created_date": "2024-08-16 23:58:36.532000", - "last_modified_date": "2024-08-16 23:58:36.532000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "2b2a07ba-c5a8-4cb8-b170-990cb941315a", - "volume_id": null - }, - { - "id": "1239cfe9-8018-4ec4-bd91-c7e71d334e9d", - "created_date": "2024-08-16 23:58:36.846000", - "last_modified_date": "2024-08-16 23:58:36.846000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", - "volume_id": null - }, - { - "id": "1239fafc-5793-40be-a775-39ef99b8e058", - "created_date": "2024-08-16 23:58:36.857000", - "last_modified_date": "2024-08-16 23:58:36.857000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "8", - "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", - "volume_id": null - }, - { - "id": "1325723f-0d13-40ab-bc4f-173ea6c7eab8", - "created_date": "2024-08-16 23:58:35.749000", - "last_modified_date": "2024-08-16 23:58:35.749000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", - "volume_id": null - }, - { - "id": "13ab0889-b2df-4b2d-b76c-99f12ed1fd87", - "created_date": "2024-08-16 23:58:36.643000", - "last_modified_date": "2024-08-16 23:58:36.643000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "104", - "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", - "volume_id": null - }, - { - "id": "13fbd46f-7dee-4ce2-af50-14528932cd17", - "created_date": "2024-08-16 23:58:36.771000", - "last_modified_date": "2024-08-16 23:58:36.771000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "9", - "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", - "volume_id": null - }, - { - "id": "14f025a9-6311-4791-a9bf-10bcb6bded2f", - "created_date": "2024-08-16 23:58:36.296000", - "last_modified_date": "2024-08-16 23:58:36.296000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "13281ef7-5945-49a9-b8f1-c5e8548c18ec", - "volume_id": null - }, - { - "id": "15735fd4-0f62-4577-937b-e6b6195588b0", - "created_date": "2024-08-16 23:58:36.925000", - "last_modified_date": "2024-08-16 23:58:36.925000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "df47cdb1-0b41-4baa-83ce-fcfbb1c2bd51", - "volume_id": null - }, - { - "id": "16a6d3f4-060f-4d49-8b93-7bb01e780d81", - "created_date": "2024-08-16 23:58:36.700000", - "last_modified_date": "2024-08-16 23:58:36.700000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "153", - "comic_id": "cc96b25e-b827-4ff0-a94a-b82d30ca883c", - "volume_id": null - }, - { - "id": "16dc5aff-f489-45f1-b971-7ca7891228bd", - "created_date": "2024-08-16 23:58:35.953000", - "last_modified_date": "2024-08-16 23:58:35.953000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "12", - "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", - "volume_id": null - }, - { - "id": "16fd76e8-4d6d-40be-98dd-87a89d05c7f2", - "created_date": "2024-08-16 23:58:36.372000", - "last_modified_date": "2024-08-16 23:58:36.372000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "b6713944-8d2d-4153-8f16-fe94cc4ee119", - "volume_id": null - }, - { - "id": "17258d6f-ba85-423c-b0d1-5449efe51ff1", - "created_date": "2024-08-16 23:58:36.547000", - "last_modified_date": "2024-08-16 23:58:36.547000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "bf59f643-455e-4b60-95b8-719d55437474", - "volume_id": null - }, - { - "id": "175ad074-40ff-4775-a532-80136e9dd31c", - "created_date": "2024-08-16 23:58:36.932000", - "last_modified_date": "2024-08-16 23:58:36.932000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "82db30ac-0622-4785-9a43-95ae37e54eaa", - "volume_id": null - }, - { - "id": "17a7750c-bbac-4447-9468-8f58e23ef78a", - "created_date": "2024-08-16 23:58:36.266000", - "last_modified_date": "2024-08-16 23:58:36.266000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "5d0fd720-7875-4f9f-86eb-07ee0723908f", - "volume_id": null - }, - { - "id": "1834defc-5ecb-46e8-b3d7-c0a574f041d3", - "created_date": "2024-08-16 23:58:35.800000", - "last_modified_date": "2024-08-16 23:58:35.800000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "b51c738d-8ba1-4451-8f89-f49c1968ac42", - "volume_id": null - }, - { - "id": "1846e8cc-e331-4c9c-8dc1-197a72ad1b42", - "created_date": "2024-08-16 23:58:36.136000", - "last_modified_date": "2024-08-16 23:58:36.136000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "c0543c9f-d712-4dce-9b59-2bf73b800b31", - "volume_id": null - }, - { - "id": "18d29aad-8c65-436b-8664-b5c0af98680c", - "created_date": "2024-08-16 23:58:36.536000", - "last_modified_date": "2024-08-16 23:58:36.536000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "2b2a07ba-c5a8-4cb8-b170-990cb941315a", - "volume_id": null - }, - { - "id": "18f01b7d-b603-477f-9eb1-dc41adf0b4c8", - "created_date": "2024-08-16 23:58:36.315000", - "last_modified_date": "2024-08-16 23:58:36.315000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "230efa0e-3aea-4b07-a05f-0f788f293d0b", - "volume_id": null - }, - { - "id": "196fe5dd-0234-4ed4-a87e-778c59d5ff19", - "created_date": "2024-08-16 23:58:36.250000", - "last_modified_date": "2024-08-16 23:58:36.250000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "639eed1d-3ccf-4bfa-a595-06b44a4e5b8f", - "volume_id": null - }, - { - "id": "19b41c73-09f7-4685-a12d-79cd23976468", - "created_date": "2024-08-16 23:58:36.861000", - "last_modified_date": "2024-08-16 23:58:36.861000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "10", - "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", - "volume_id": null - }, - { - "id": "19cdde53-4042-446b-ab07-8c0392527d62", - "created_date": "2024-08-16 23:58:36.842000", - "last_modified_date": "2024-08-16 23:58:36.842000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "9fcdd9a5-f1fb-4421-a352-9da8e2c12f81", - "volume_id": null - }, - { - "id": "19dc8eac-7678-44af-aa72-12583199de91", - "created_date": "2024-08-16 23:58:36.749000", - "last_modified_date": "2024-08-16 23:58:36.749000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "4f0e16a3-452f-43cd-9834-d0f4d2d5fca0", - "volume_id": null - }, - { - "id": "1a11f7aa-b5f1-47ac-b447-209c0b62dbe9", - "created_date": "2024-08-16 23:58:36.453000", - "last_modified_date": "2024-08-16 23:58:36.453000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "24", - "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", - "volume_id": null - }, - { - "id": "1b306d28-0d69-4a2f-8d8a-f61410d89ca4", - "created_date": "2024-08-16 23:58:36.865000", - "last_modified_date": "2024-08-16 23:58:36.865000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "12", - "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", - "volume_id": null - }, - { - "id": "1ba383b4-74f9-435e-ab3a-c76a1d7456c4", - "created_date": "2024-08-16 23:58:36.406000", - "last_modified_date": "2024-08-16 23:58:36.406000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "21", - "comic_id": "ed58b16e-0701-4373-befe-39118bc2d4cb", - "volume_id": null - }, - { - "id": "1c030174-7f55-497b-8240-26a312a1de89", - "created_date": "2024-08-16 23:58:36.737000", - "last_modified_date": "2024-08-16 23:58:36.737000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "8690631a-e99c-44be-887c-8fd2bb222ee1", - "volume_id": null - }, - { - "id": "1cea0ef0-62ae-44c0-b18e-3189a2db0e87", - "created_date": "2024-08-16 23:58:36.567000", - "last_modified_date": "2024-08-16 23:58:36.567000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "45bd0c8a-845f-4ab3-9281-c31e5a3d4472", - "volume_id": null - }, - { - "id": "1d11de5a-fe99-4364-8749-50d2ed083bf5", - "created_date": "2024-08-16 23:58:36.537000", - "last_modified_date": "2024-08-16 23:58:36.537000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "f4cb5b24-00ea-4249-9d09-45432c168b8f", - "volume_id": null - }, - { - "id": "1d23e7a0-69e2-492a-9a6e-53d022f18aa6", - "created_date": "2024-08-16 23:58:35.677000", - "last_modified_date": "2024-08-16 23:58:35.677000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "10", - "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", - "volume_id": null - }, - { - "id": "1d3ecddf-cf6e-4d5b-9b94-151e34c3a69c", - "created_date": "2024-08-16 23:58:36.599000", - "last_modified_date": "2024-08-16 23:58:36.599000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "20", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "1dba942b-90fc-417a-8b3d-d0987ef4cc4d", - "created_date": "2024-08-16 23:58:36.279000", - "last_modified_date": "2024-08-16 23:58:36.279000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "3e96be30-f58f-459d-bb97-cfa574b9487c", - "volume_id": null - }, - { - "id": "1de6b0ef-1a18-482b-a9ab-443409ec964d", - "created_date": "2024-08-16 23:58:35.845000", - "last_modified_date": "2024-08-16 23:58:35.845000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "9", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "1e4f0a5b-91eb-4fff-882c-97ecdc414a1e", - "created_date": "2024-08-16 23:58:36.744000", - "last_modified_date": "2024-08-16 23:58:36.744000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "fffaa1a6-5c3d-4deb-b759-35de74c65958", - "volume_id": null - }, - { - "id": "1e6f4148-351b-4af8-a617-ccbea38d22de", - "created_date": "2024-08-16 23:58:35.595000", - "last_modified_date": "2024-08-16 23:58:35.595000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "5dbe4d8b-331a-41ad-bcdc-01196dc1d58d", - "volume_id": null - }, - { - "id": "1ea7ddf0-29ba-4e0b-b451-211bcd54d449", - "created_date": "2024-08-16 23:58:37.079000", - "last_modified_date": "2024-08-16 23:58:37.079000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "b4c866cb-9461-4aa4-bc3b-ef3e63848775", - "volume_id": null - }, - { - "id": "1ecb7f36-f6d7-4f29-ae22-df5c3fc0e2ab", - "created_date": "2024-08-16 23:58:36.091000", - "last_modified_date": "2024-08-16 23:58:36.091000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "d99f789a-0d9c-4b12-bf1f-3b090fb0f1b8", - "volume_id": null - }, - { - "id": "1f785cdc-fc07-4e68-b2b9-aa4c757b9703", - "created_date": "2024-08-16 23:58:36.598000", - "last_modified_date": "2024-08-16 23:58:36.598000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "19", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "20042e8a-243f-43f3-99d4-3a2916116786", - "created_date": "2024-08-16 23:58:35.908000", - "last_modified_date": "2024-08-16 23:58:35.908000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "b73e5e2f-60e4-42fe-94bf-44fe3755b8b8", - "volume_id": null - }, - { - "id": "2060b03e-1ba7-4022-9a5a-5a0140b91e96", - "created_date": "2024-08-16 23:58:36.690000", - "last_modified_date": "2024-08-16 23:58:36.690000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "13", - "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", - "volume_id": null - }, - { - "id": "2122df9a-897f-41c8-b5e3-dcf3e758c007", - "created_date": "2024-08-16 23:58:36.527000", - "last_modified_date": "2024-08-16 23:58:36.527000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "26", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "213fd3d0-8017-4f78-b217-aaeda8a6519f", - "created_date": "2024-08-16 23:58:36.628000", - "last_modified_date": "2024-08-16 23:58:36.628000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "0e6688d2-f347-4800-83b1-1f094b658084", - "volume_id": null - }, - { - "id": "2192ceb5-2129-4a27-ab5e-486e5f2906e2", - "created_date": "2024-08-16 23:58:36.587000", - "last_modified_date": "2024-08-16 23:58:36.587000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "13", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "2211c609-b6c9-4435-8e93-7c509ab79b20", - "created_date": "2024-08-16 23:58:35.608000", - "last_modified_date": "2024-08-16 23:58:35.608000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "5dbe4d8b-331a-41ad-bcdc-01196dc1d58d", - "volume_id": null - }, - { - "id": "222e75f6-ebc9-4892-a6dc-f26c4adbb9a7", - "created_date": "2024-08-16 23:58:36.028000", - "last_modified_date": "2024-08-16 23:58:36.028000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "17", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", - "volume_id": null - }, - { - "id": "2234d1bb-046b-4b0d-a779-3cf549370bb7", - "created_date": "2024-08-16 23:58:36.839000", - "last_modified_date": "2024-08-16 23:58:36.839000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "9fcdd9a5-f1fb-4421-a352-9da8e2c12f81", - "volume_id": null - }, - { - "id": "22d0814c-11b7-4117-a831-a706ca3ed1d4", - "created_date": "2024-08-16 23:58:36.566000", - "last_modified_date": "2024-08-16 23:58:36.566000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "45bd0c8a-845f-4ab3-9281-c31e5a3d4472", - "volume_id": null - }, - { - "id": "23a19e43-d4b9-4ac3-b9e1-17ef97be760e", - "created_date": "2024-08-16 23:58:36.612000", - "last_modified_date": "2024-08-16 23:58:36.612000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "28", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "23b6c8ff-8287-4d0c-9bc5-36bcad06fa06", - "created_date": "2024-08-16 23:58:36.370000", - "last_modified_date": "2024-08-16 23:58:36.370000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "b6713944-8d2d-4153-8f16-fe94cc4ee119", - "volume_id": null - }, - { - "id": "23be1e0b-49cb-4d24-b70a-30d612f9c79a", - "created_date": "2024-08-16 23:58:36.271000", - "last_modified_date": "2024-08-16 23:58:36.271000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "39536665-0c93-439f-97d4-be1f40da34dd", - "volume_id": null - }, - { - "id": "2419876b-d084-4d80-953e-8f4104c3ca04", - "created_date": "2024-08-16 23:58:36.722000", - "last_modified_date": "2024-08-16 23:58:36.722000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "25841b05-c246-4484-9ef2-71f8dcdb39ed", - "volume_id": null - }, - { - "id": "24246364-1514-4c54-914a-f02fdf2bb111", - "created_date": "2024-08-16 23:58:36.970000", - "last_modified_date": "2024-08-16 23:58:36.970000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "8", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "242cac83-e935-43f9-a216-2bc46c2b25b2", - "created_date": "2024-08-16 23:58:36.820000", - "last_modified_date": "2024-08-16 23:58:36.820000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "bba808d8-ede8-49fe-9ed5-3c23c7ca0f3c", - "volume_id": null - }, - { - "id": "260bd792-67ca-47b7-a930-e888956c75b9", - "created_date": "2024-08-16 23:58:36.508000", - "last_modified_date": "2024-08-16 23:58:36.508000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "17", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "260f0244-dbc9-4cc8-8fec-37664f4ebfe0", - "created_date": "2024-08-16 23:58:35.733000", - "last_modified_date": "2024-08-16 23:58:35.733000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "20", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "263a2c72-1834-4278-935a-076cd299544c", - "created_date": "2024-08-16 23:58:36.992000", - "last_modified_date": "2024-08-16 23:58:36.992000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "21", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "2645f158-2c38-40ac-9c66-0b680d1a21a3", - "created_date": "2024-08-16 23:58:36.312000", - "last_modified_date": "2024-08-16 23:58:36.312000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "60deca87-7b2a-4412-855f-01a6ccaeea56", - "volume_id": null - }, - { - "id": "2687c901-206c-4cb2-a0e9-1df1c0ed48ef", - "created_date": "2024-08-16 23:58:36.890000", - "last_modified_date": "2024-08-16 23:58:36.890000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "12", - "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", - "volume_id": null - }, - { - "id": "26e57eaa-9566-482a-842e-b00102f280b9", - "created_date": "2024-08-16 23:58:35.939000", - "last_modified_date": "2024-08-16 23:58:35.939000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "8", - "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", - "volume_id": null - }, - { - "id": "283152f1-38e9-4b2e-ac0b-f80dcdf8b8ad", - "created_date": "2024-08-16 23:58:37.042000", - "last_modified_date": "2024-08-16 23:58:37.042000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "525", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "2886cc4b-8f21-4d0a-b4ce-18dd3d4ae939", - "created_date": "2024-08-16 23:58:36.146000", - "last_modified_date": "2024-08-16 23:58:36.146000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "c0543c9f-d712-4dce-9b59-2bf73b800b31", - "volume_id": null - }, - { - "id": "28e70d59-638a-45da-b273-ec55085cd053", - "created_date": "2024-08-16 23:58:36.682000", - "last_modified_date": "2024-08-16 23:58:36.682000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "9", - "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", - "volume_id": null - }, - { - "id": "292051bb-cb8b-4989-8e00-75b6a677faaa", - "created_date": "2024-08-16 23:58:36.874000", - "last_modified_date": "2024-08-16 23:58:36.874000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", - "volume_id": null - }, - { - "id": "29526a85-abec-4fc2-91a3-bce5b8f08a6f", - "created_date": "2024-08-16 23:58:36.093000", - "last_modified_date": "2024-08-16 23:58:36.093000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "de7ef7f5-daf8-4dfd-b8de-973c902a7df0", - "volume_id": null - }, - { - "id": "299be889-336a-49e9-bba5-6c288069dee0", - "created_date": "2024-08-16 23:58:36.695000", - "last_modified_date": "2024-08-16 23:58:36.695000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "16", - "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", - "volume_id": null - }, - { - "id": "2a1bf817-518c-441d-a0ed-733a538d27b9", - "created_date": "2024-08-16 23:58:36.552000", - "last_modified_date": "2024-08-16 23:58:36.552000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "bf59f643-455e-4b60-95b8-719d55437474", - "volume_id": null - }, - { - "id": "2b54ecce-d571-44fe-a632-94c8f55b0930", - "created_date": "2024-08-16 23:58:36.678000", - "last_modified_date": "2024-08-16 23:58:36.678000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", - "volume_id": null - }, - { - "id": "2bc64196-cbd5-4be8-bd83-0a3a05240759", - "created_date": "2024-08-16 23:58:36.289000", - "last_modified_date": "2024-08-16 23:58:36.289000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "358f99c2-93f2-489d-83a4-03267fad8597", - "volume_id": null - }, - { - "id": "2bebd24c-6711-46cc-8a12-fdfa7f65b494", - "created_date": "2024-08-16 23:58:36.428000", - "last_modified_date": "2024-08-16 23:58:36.428000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "84", - "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", - "volume_id": null - }, - { - "id": "2c3f91d2-ce23-45f9-8908-d04528c16ba5", - "created_date": "2024-08-16 23:58:35.866000", - "last_modified_date": "2024-08-16 23:58:35.866000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "16", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "2c6609da-4592-4c50-94f0-a33a77dfce5c", - "created_date": "2024-08-16 23:58:36.773000", - "last_modified_date": "2024-08-16 23:58:36.773000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "11", - "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", - "volume_id": null - }, - { - "id": "2ca61b08-8840-4116-a505-517c9c5c5853", - "created_date": "2024-08-16 23:58:37.110000", - "last_modified_date": "2024-08-16 23:58:37.110000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "639f7e71-1012-49a6-bc3a-1ac7b1de3084", - "volume_id": null - }, - { - "id": "2cadaaea-4123-4d12-84e8-569ff3f16dd7", - "created_date": "2024-08-16 23:58:36.077000", - "last_modified_date": "2024-08-16 23:58:36.077000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "14", - "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", - "volume_id": null - }, - { - "id": "2cc03de7-94e8-41f4-8b0c-6d66008c445a", - "created_date": "2024-08-16 23:58:35.946000", - "last_modified_date": "2024-08-16 23:58:35.946000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "10", - "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", - "volume_id": null - }, - { - "id": "2d1f0ade-a5de-486c-bd6b-f7fa09c7340d", - "created_date": "2024-08-16 23:58:36.253000", - "last_modified_date": "2024-08-16 23:58:36.253000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "639eed1d-3ccf-4bfa-a595-06b44a4e5b8f", - "volume_id": null - }, - { - "id": "2d73deeb-f046-41e1-beaf-f7a6d7d57dc8", - "created_date": "2024-08-16 23:58:35.673000", - "last_modified_date": "2024-08-16 23:58:35.673000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "9", - "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", - "volume_id": null - }, - { - "id": "2f634ce1-2eb5-4b00-93f0-98c3551aa823", - "created_date": "2024-08-16 23:58:36.431000", - "last_modified_date": "2024-08-16 23:58:36.431000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "86", - "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", - "volume_id": null - }, - { - "id": "2f724333-b949-4f5d-88a0-ff0c951ac22e", - "created_date": "2024-08-16 23:58:36.245000", - "last_modified_date": "2024-08-16 23:58:36.245000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "639eed1d-3ccf-4bfa-a595-06b44a4e5b8f", - "volume_id": null - }, - { - "id": "2fc5ec29-4f5e-44a3-aacd-e6b553cd302d", - "created_date": "2024-08-16 23:58:35.810000", - "last_modified_date": "2024-08-16 23:58:35.810000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "89c5ea13-997a-4831-87cc-ee76ea05c71e", - "volume_id": null - }, - { - "id": "3012df9b-2cf2-4b7c-84c1-91c130e7ffee", - "created_date": "2024-08-16 23:58:36.934000", - "last_modified_date": "2024-08-16 23:58:36.934000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "20", - "comic_id": "ab909424-4ab4-4084-a47d-08ab865047e7", - "volume_id": null - }, - { - "id": "3140e945-0bc2-4c70-836e-32bcf1ac4040", - "created_date": "2024-08-16 23:58:36.911000", - "last_modified_date": "2024-08-16 23:58:36.911000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "a08aef89-634c-494c-9def-73ff7e416464", - "volume_id": null - }, - { - "id": "3209c451-135f-44e8-bcce-823729c8c624", - "created_date": "2024-08-16 23:58:36.648000", - "last_modified_date": "2024-08-16 23:58:36.648000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "107", - "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", - "volume_id": null - }, - { - "id": "322fff97-5ca7-40c0-a70c-72c2c996afb4", - "created_date": "2024-08-16 23:58:36.609000", - "last_modified_date": "2024-08-16 23:58:36.609000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "26", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "32938ea3-4f1c-4e09-a0d7-cbfa39bf1326", - "created_date": "2024-08-16 23:58:36.242000", - "last_modified_date": "2024-08-16 23:58:36.242000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "ff81bcf6-b368-4264-872c-544e85ec80e8", - "volume_id": null - }, - { - "id": "32b89dc7-9944-4beb-acbe-15893519e9f3", - "created_date": "2024-08-16 23:58:36.081000", - "last_modified_date": "2024-08-16 23:58:36.081000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "11bc20d5-bfeb-4825-9e0f-3d6954020b07", - "volume_id": null - }, - { - "id": "32bbe897-1484-4cb4-b5b3-7f2025cf0271", - "created_date": "2024-08-16 23:58:36.881000", - "last_modified_date": "2024-08-16 23:58:36.881000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", - "volume_id": null - }, - { - "id": "32c7f396-1ec0-4bf7-a970-f50e9d4733a6", - "created_date": "2024-08-16 23:58:36.657000", - "last_modified_date": "2024-08-16 23:58:36.657000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "111", - "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", - "volume_id": null - }, - { - "id": "32f52206-19ac-416c-9f37-68ca28f7ce6f", - "created_date": "2024-08-16 23:58:36.995000", - "last_modified_date": "2024-08-16 23:58:36.995000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "23", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "33ada9ac-794c-4af6-8d50-2126546daf68", - "created_date": "2024-08-16 23:58:36.277000", - "last_modified_date": "2024-08-16 23:58:36.277000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "3e96be30-f58f-459d-bb97-cfa574b9487c", - "volume_id": null - }, - { - "id": "33c30faa-9391-4025-8791-06526a527923", - "created_date": "2024-08-16 23:58:36.380000", - "last_modified_date": "2024-08-16 23:58:36.380000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "af866a6a-2b51-499a-aa2d-b46743aabafd", - "volume_id": null - }, - { - "id": "350c0c70-302c-4b2e-8eba-843931ebe8ec", - "created_date": "2024-08-16 23:58:35.656000", - "last_modified_date": "2024-08-16 23:58:35.656000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", - "volume_id": null - }, - { - "id": "35123684-12a0-4bea-be34-ab4c7cf3508b", - "created_date": "2024-08-16 23:58:36.500000", - "last_modified_date": "2024-08-16 23:58:36.500000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "12", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "3577b0bc-e00f-43d5-bccc-d5c543d4cd1d", - "created_date": "2024-08-16 23:58:36.038000", - "last_modified_date": "2024-08-16 23:58:36.038000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "bf23d317-f5b6-4cf8-8a05-4397888a82c9", - "volume_id": null - }, - { - "id": "357d559d-e57b-4d22-9a51-7391105e38f1", - "created_date": "2024-08-16 23:58:36.849000", - "last_modified_date": "2024-08-16 23:58:36.849000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", - "volume_id": null - }, - { - "id": "35c77126-e600-4973-9f02-8c136ef90f72", - "created_date": "2024-08-16 23:58:36.448000", - "last_modified_date": "2024-08-16 23:58:36.448000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "22", - "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", - "volume_id": null - }, - { - "id": "36d5081d-53d7-4eb8-836b-bf3cce2bf59b", - "created_date": "2024-08-16 23:58:36.355000", - "last_modified_date": "2024-08-16 23:58:36.355000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "c91ec109-0b4d-4fd6-995f-1a828958493f", - "volume_id": null - }, - { - "id": "370a5e12-83d8-4905-9151-89e04c393749", - "created_date": "2024-08-16 23:58:35.885000", - "last_modified_date": "2024-08-16 23:58:35.885000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "22", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "371d04f6-46fb-4233-a21a-7be740920d8a", - "created_date": "2024-08-16 23:58:36.317000", - "last_modified_date": "2024-08-16 23:58:36.317000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "28adbced-8575-4858-8150-796857bb4129", - "volume_id": null - }, - { - "id": "37c016f1-aa6a-4ba9-bb78-e1b7f6714052", - "created_date": "2024-08-16 23:58:36.183000", - "last_modified_date": "2024-08-16 23:58:36.183000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "8", - "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", - "volume_id": null - }, - { - "id": "37c234d6-6500-4f86-9be8-2ffbbd6a9ee4", - "created_date": "2024-08-16 23:58:36.906000", - "last_modified_date": "2024-08-16 23:58:36.906000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "9aac6484-4c06-4286-815a-219aad25cc74", - "volume_id": null - }, - { - "id": "37d0d861-f4e7-440f-81c9-9910fc9ef978", - "created_date": "2024-08-16 23:58:36.438000", - "last_modified_date": "2024-08-16 23:58:36.438000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "90", - "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", - "volume_id": null - }, - { - "id": "385c5087-bcb8-4cd8-bed5-ffbb9d5e2051", - "created_date": "2024-08-16 23:58:36.859000", - "last_modified_date": "2024-08-16 23:58:36.859000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "9", - "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", - "volume_id": null - }, - { - "id": "38e78ff4-f667-4a0e-9fbf-b3aa0d952670", - "created_date": "2024-08-16 23:58:36.591000", - "last_modified_date": "2024-08-16 23:58:36.591000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "16", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "3900e943-fa26-4ec0-a224-6aa2e42b86f6", - "created_date": "2024-08-16 23:58:36.524000", - "last_modified_date": "2024-08-16 23:58:36.524000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "24", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "390848ae-6ef3-4834-8e1c-c461a77da058", - "created_date": "2024-08-16 23:58:35.873000", - "last_modified_date": "2024-08-16 23:58:35.873000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "18", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "39271c8d-b64c-48dc-a239-fca5a34be74f", - "created_date": "2024-08-16 23:58:36.388000", - "last_modified_date": "2024-08-16 23:58:36.388000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "209", - "comic_id": "d24801e2-fbfe-4497-873f-4d8edb182ae4", - "volume_id": null - }, - { - "id": "39483544-36d0-44ff-ba54-7e0737db9aa8", - "created_date": "2024-08-16 23:58:36.113000", - "last_modified_date": "2024-08-16 23:58:36.113000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "0f0bfd13-f6f0-436c-ad74-5e40ea6a0cf8", - "volume_id": null - }, - { - "id": "3a4f4fd1-d004-461a-a53f-75c9b193c6ae", - "created_date": "2024-08-16 23:58:36.392000", - "last_modified_date": "2024-08-16 23:58:36.392000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "211", - "comic_id": "d24801e2-fbfe-4497-873f-4d8edb182ae4", - "volume_id": null - }, - { - "id": "3a542153-69ee-4c16-86ae-0d76f8d49fc4", - "created_date": "2024-08-16 23:58:36.829000", - "last_modified_date": "2024-08-16 23:58:36.829000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "8e293af3-05c6-4dcc-9cbf-b87512ec975b", - "volume_id": null - }, - { - "id": "3a707cb5-4464-486a-a63b-a57cf0dd851d", - "created_date": "2024-08-16 23:58:37.045000", - "last_modified_date": "2024-08-16 23:58:37.045000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "a5987e5c-0245-484d-aeb8-4b5195800d66", - "volume_id": null - }, - { - "id": "3a95141f-2ff6-4a7d-9b61-891c2284212e", - "created_date": "2024-08-16 23:58:36.884000", - "last_modified_date": "2024-08-16 23:58:36.884000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "9", - "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", - "volume_id": null - }, - { - "id": "3ac8c11e-0102-4555-a06c-e7e0eed1dddf", - "created_date": "2024-08-16 23:58:36.107000", - "last_modified_date": "2024-08-16 23:58:36.107000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "9b924cdc-8959-41e0-a84d-f3e61bbeac44", - "volume_id": null - }, - { - "id": "3b125f13-636b-4393-97e8-ea9bd23ed191", - "created_date": "2024-08-16 23:58:36.304000", - "last_modified_date": "2024-08-16 23:58:36.304000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "13281ef7-5945-49a9-b8f1-c5e8548c18ec", - "volume_id": null - }, - { - "id": "3c25513b-88a2-43af-95b8-9666fd13e232", - "created_date": "2024-08-16 23:58:36.017000", - "last_modified_date": "2024-08-16 23:58:36.017000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "12", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", - "volume_id": null - }, - { - "id": "3c65afdf-0a18-4626-9bca-aceb75a9d22a", - "created_date": "2024-08-16 23:58:36.826000", - "last_modified_date": "2024-08-16 23:58:36.826000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "8e293af3-05c6-4dcc-9cbf-b87512ec975b", - "volume_id": null - }, - { - "id": "3d4ae0be-d485-4602-a00d-b27a0bee1863", - "created_date": "2024-08-16 23:58:36.851000", - "last_modified_date": "2024-08-16 23:58:36.851000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", - "volume_id": null - }, - { - "id": "3d510703-f76b-47c1-8fd5-9e5a47016bd1", - "created_date": "2024-08-16 23:58:35.709000", - "last_modified_date": "2024-08-16 23:58:35.709000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "12", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "3e32c255-ec26-424f-a72a-1e461ae14d6b", - "created_date": "2024-08-16 23:58:35.684000", - "last_modified_date": "2024-08-16 23:58:35.684000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "3e58d82c-3421-4a59-9be3-5c1a3a05e00d", - "created_date": "2024-08-16 23:58:37.097000", - "last_modified_date": "2024-08-16 23:58:37.097000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "14a359a2-c928-4e14-9fb2-249777e6a13a", - "volume_id": null - }, - { - "id": "3ec534ad-11b3-4a20-9ff0-2a40d7d6d911", - "created_date": "2024-08-16 23:58:36.977000", - "last_modified_date": "2024-08-16 23:58:36.977000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "12", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "3f4a86d0-7479-4947-bcf2-80ecb63f2b88", - "created_date": "2024-08-16 23:58:36.458000", - "last_modified_date": "2024-08-16 23:58:36.458000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "25", - "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", - "volume_id": null - }, - { - "id": "3f50c2fa-a315-4672-b0c9-9ef752fce511", - "created_date": "2024-08-16 23:58:35.895000", - "last_modified_date": "2024-08-16 23:58:35.895000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "26", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "3f7fda95-213d-41a9-bb40-7c072b15a770", - "created_date": "2024-08-16 23:58:37.005000", - "last_modified_date": "2024-08-16 23:58:37.005000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "503", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "3ffa1cf3-930e-42a5-b890-ea01db47f849", - "created_date": "2024-08-16 23:58:36.807000", - "last_modified_date": "2024-08-16 23:58:36.807000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "61a4d2fe-44b2-41bb-a19c-f7be95d5e195", - "volume_id": null - }, - { - "id": "400fd0ab-4031-439e-962d-b89c541f7e3f", - "created_date": "2024-08-16 23:58:37.035000", - "last_modified_date": "2024-08-16 23:58:37.035000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "521", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "408e7cbc-17fd-4f30-985e-f43302283ff0", - "created_date": "2024-08-16 23:58:35.975000", - "last_modified_date": "2024-08-16 23:58:35.975000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", - "volume_id": null - }, - { - "id": "40a13eb2-1854-4f64-b4bd-ef91bcad4b79", - "created_date": "2024-08-16 23:58:36.803000", - "last_modified_date": "2024-08-16 23:58:36.803000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "14", - "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", - "volume_id": null - }, - { - "id": "40a4bf3b-9f74-4d04-9b20-221ea922a462", - "created_date": "2024-08-16 23:58:37.013000", - "last_modified_date": "2024-08-16 23:58:37.013000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "508", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "40d61175-aa4e-40d0-8161-96a22b75aaa5", - "created_date": "2024-08-16 23:58:36.243000", - "last_modified_date": "2024-08-16 23:58:36.243000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "8", - "comic_id": "ff81bcf6-b368-4264-872c-544e85ec80e8", - "volume_id": null - }, - { - "id": "410e86be-34cf-47e7-a932-9141c4aea99c", - "created_date": "2024-08-16 23:58:36.502000", - "last_modified_date": "2024-08-16 23:58:36.502000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "13", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "41629dd5-eb2a-4d8f-97fd-f1f344e6a4fc", - "created_date": "2024-08-16 23:58:36.403000", - "last_modified_date": "2024-08-16 23:58:36.403000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "19", - "comic_id": "ed58b16e-0701-4373-befe-39118bc2d4cb", - "volume_id": null - }, - { - "id": "416b0d7b-44f9-4be7-be5f-8019ca7adb5d", - "created_date": "2024-08-16 23:58:36.096000", - "last_modified_date": "2024-08-16 23:58:36.096000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "12fde8f7-8ce3-4398-8095-5bb9b5d9508d", - "volume_id": null - }, - { - "id": "41cfccad-f483-473d-b30f-d61e6d1cc4ed", - "created_date": "2024-08-16 23:58:35.883000", - "last_modified_date": "2024-08-16 23:58:35.883000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "21", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "421c3f14-bc65-4dcf-bb40-e8d7172d8336", - "created_date": "2024-08-16 23:58:36.687000", - "last_modified_date": "2024-08-16 23:58:36.687000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "12", - "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", - "volume_id": null - }, - { - "id": "422a07a1-c937-4c38-9a5f-598675a9eee3", - "created_date": "2024-08-16 23:58:36.202000", - "last_modified_date": "2024-08-16 23:58:36.202000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", - "volume_id": null - }, - { - "id": "447fe352-7a1f-4333-8a56-78fb6db5f5bb", - "created_date": "2024-08-16 23:58:36.190000", - "last_modified_date": "2024-08-16 23:58:36.190000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "12", - "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", - "volume_id": null - }, - { - "id": "448e7efb-75b6-47e3-b901-2a2dc08f027b", - "created_date": "2024-08-16 23:58:36.563000", - "last_modified_date": "2024-08-16 23:58:36.563000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "100b82bf-c134-40d9-bcc2-a8b345030b8d", - "volume_id": null - }, - { - "id": "44e71984-dac7-4f7b-b268-f096d72a9d1b", - "created_date": "2024-08-16 23:58:36.434000", - "last_modified_date": "2024-08-16 23:58:36.434000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "87", - "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", - "volume_id": null - }, - { - "id": "4540db93-a3e1-4166-baa6-ef3be86d4b99", - "created_date": "2024-08-16 23:58:37.064000", - "last_modified_date": "2024-08-16 23:58:37.064000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "20", - "comic_id": "6fd2c6fd-f8f1-4f13-892d-887b315fa4a3", - "volume_id": null - }, - { - "id": "4561aab5-ab4d-40c7-8c6f-1c92802274cb", - "created_date": "2024-08-16 23:58:36.766000", - "last_modified_date": "2024-08-16 23:58:36.766000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", - "volume_id": null - }, - { - "id": "459b1bd9-abf1-4bb5-9da7-5fe5519c48dd", - "created_date": "2024-08-16 23:58:36.685000", - "last_modified_date": "2024-08-16 23:58:36.685000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "11", - "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", - "volume_id": null - }, - { - "id": "45bccdd5-dd09-45d7-8f21-60687e8a0d14", - "created_date": "2024-08-16 23:58:35.892000", - "last_modified_date": "2024-08-16 23:58:35.892000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "25", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "46374dfc-9e16-43f4-94dd-66d082e56ada", - "created_date": "2024-08-16 23:58:36.291000", - "last_modified_date": "2024-08-16 23:58:36.291000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "358f99c2-93f2-489d-83a4-03267fad8597", - "volume_id": null - }, - { - "id": "46a1c4dd-5a08-42f2-9c9b-3141882375a4", - "created_date": "2024-08-16 23:58:35.692000", - "last_modified_date": "2024-08-16 23:58:35.692000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "46d42039-52d4-472a-86ca-fb586695b060", - "created_date": "2024-08-16 23:58:36.818000", - "last_modified_date": "2024-08-16 23:58:36.818000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "12802b17-452a-4533-a7ab-fd94f19be123", - "volume_id": null - }, - { - "id": "479b37c2-6768-4fdb-a8ec-c0778000b9bc", - "created_date": "2024-08-16 23:58:36.948000", - "last_modified_date": "2024-08-16 23:58:36.948000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "35894d94-1fb5-49de-ba77-cf2626cda939", - "volume_id": null - }, - { - "id": "479eed54-b689-46d5-ae55-dfa38c11e849", - "created_date": "2024-08-16 23:58:35.869000", - "last_modified_date": "2024-08-16 23:58:35.869000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "17", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "47b608bc-31eb-452e-84cc-657a3a8dccb7", - "created_date": "2024-08-16 23:58:35.707000", - "last_modified_date": "2024-08-16 23:58:35.707000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "11", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "4829d6ac-6182-4fc1-8019-a820ccbf50a3", - "created_date": "2024-08-16 23:58:37.028000", - "last_modified_date": "2024-08-16 23:58:37.028000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "517", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "483dc56e-2018-4770-ae8a-0b10aa0d6dc8", - "created_date": "2024-08-16 23:58:36.589000", - "last_modified_date": "2024-08-16 23:58:36.589000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "15", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "4844f763-362d-4be8-afdf-0622175f509c", - "created_date": "2024-08-16 23:58:36.417000", - "last_modified_date": "2024-08-16 23:58:36.417000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "6fff100e-050b-4da7-bdfa-10675dbab84e", - "volume_id": null - }, - { - "id": "48496963-fa12-426f-a79a-bb181cc2f665", - "created_date": "2024-08-16 23:58:36.022000", - "last_modified_date": "2024-08-16 23:58:36.022000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "14", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", - "volume_id": null - }, - { - "id": "487c1c5f-73b4-49e5-bd9c-6e5b687587e1", - "created_date": "2024-08-16 23:58:36.495000", - "last_modified_date": "2024-08-16 23:58:36.495000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "9", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "487f6363-b673-4fc1-9b83-c8a8d44f491a", - "created_date": "2024-08-16 23:58:36.987000", - "last_modified_date": "2024-08-16 23:58:36.987000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "18", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "48d66201-cf4b-411a-be42-ade3e74add24", - "created_date": "2024-08-16 23:58:36.060000", - "last_modified_date": "2024-08-16 23:58:36.060000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", - "volume_id": null - }, - { - "id": "4900f694-f054-48cb-95f9-48f9d44d24b1", - "created_date": "2024-08-16 23:58:36.785000", - "last_modified_date": "2024-08-16 23:58:36.785000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", - "volume_id": null - }, - { - "id": "4929cb43-8e8c-4ab9-a9ec-fd79be4f81ee", - "created_date": "2024-08-16 23:58:36.460000", - "last_modified_date": "2024-08-16 23:58:36.460000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "26", - "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", - "volume_id": null - }, - { - "id": "4944e5e3-1720-46bf-b8f2-57b26263b0d9", - "created_date": "2024-08-16 23:58:37.082000", - "last_modified_date": "2024-08-16 23:58:37.082000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "b4c866cb-9461-4aa4-bc3b-ef3e63848775", - "volume_id": null - }, - { - "id": "4946fe63-d1e0-4d26-8cf0-586dbb64b0f7", - "created_date": "2024-08-16 23:58:36.223000", - "last_modified_date": "2024-08-16 23:58:36.223000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "fe7fb34e-5ff2-4f6d-a649-0be19a368f46", - "volume_id": null - }, - { - "id": "4974def3-3b02-4b56-9e1c-e61c490fb63c", - "created_date": "2024-08-16 23:58:37.062000", - "last_modified_date": "2024-08-16 23:58:37.062000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "19", - "comic_id": "6fd2c6fd-f8f1-4f13-892d-887b315fa4a3", - "volume_id": null - }, - { - "id": "49e06d24-742d-47b0-856f-ee14e7bf782d", - "created_date": "2024-08-16 23:58:36.381000", - "last_modified_date": "2024-08-16 23:58:36.381000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "af866a6a-2b51-499a-aa2d-b46743aabafd", - "volume_id": null - }, - { - "id": "49f18f8a-3ec1-4c35-8d0d-7abc764d916c", - "created_date": "2024-08-16 23:58:35.911000", - "last_modified_date": "2024-08-16 23:58:35.911000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", - "volume_id": null - }, - { - "id": "4aa8b358-8fd3-45e7-8180-37c98be19fa2", - "created_date": "2024-08-16 23:58:36.497000", - "last_modified_date": "2024-08-16 23:58:36.497000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "10", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "4abcceaf-7d7e-4faa-baac-6151cc963baa", - "created_date": "2024-08-16 23:58:36.321000", - "last_modified_date": "2024-08-16 23:58:36.321000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "7c110b15-bbfc-472e-830b-b8db6ddb274e", - "volume_id": null - }, - { - "id": "4ac31ebb-0a7d-47f7-8a7f-c702207fdc02", - "created_date": "2024-08-16 23:58:36.105000", - "last_modified_date": "2024-08-16 23:58:36.105000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "9b924cdc-8959-41e0-a84d-f3e61bbeac44", - "volume_id": null - }, - { - "id": "4ac45ee8-b795-447e-9aa1-85969f4f6c07", - "created_date": "2024-08-16 23:58:36.604000", - "last_modified_date": "2024-08-16 23:58:36.604000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "23", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "4af49b06-9c1d-4e0b-b9b9-8046422bb642", - "created_date": "2024-08-16 23:58:36.482000", - "last_modified_date": "2024-08-16 23:58:36.482000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "4b1c6dda-5e3d-4199-bd66-3df62a7dc317", - "created_date": "2024-08-16 23:58:36.607000", - "last_modified_date": "2024-08-16 23:58:36.607000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "25", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "4b26c23a-8707-4f9f-a7c1-28efb1702adc", - "created_date": "2024-08-16 23:58:35.660000", - "last_modified_date": "2024-08-16 23:58:35.660000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", - "volume_id": null - }, - { - "id": "4bb55b72-636c-4666-adf6-d9b54b0cffdd", - "created_date": "2024-08-16 23:58:35.848000", - "last_modified_date": "2024-08-16 23:58:35.848000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "10", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "4bbc894d-ddce-4eca-a95f-5556a13a6ff1", - "created_date": "2024-08-16 23:58:37.116000", - "last_modified_date": "2024-08-16 23:58:37.116000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "ea903b99-4032-4b4e-add4-177e051733a8", - "volume_id": null - }, - { - "id": "4c2b1a03-5725-4c9f-b4ce-e6f7d1e2ad6e", - "created_date": "2024-08-16 23:58:36.993000", - "last_modified_date": "2024-08-16 23:58:36.993000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "22", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "4c2b775d-e3b5-4ecb-928c-b4723403dd42", - "created_date": "2024-08-16 23:58:36.742000", - "last_modified_date": "2024-08-16 23:58:36.742000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "a557dbbc-2af1-4b56-8588-8fe42cf5c454", - "volume_id": null - }, - { - "id": "4c52c4bc-ac2a-49fb-8f0b-566acf0e3168", - "created_date": "2024-08-16 23:58:35.727000", - "last_modified_date": "2024-08-16 23:58:35.727000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "18", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "4d179006-ab1e-41cf-b366-7a658f1357d1", - "created_date": "2024-08-16 23:58:36.728000", - "last_modified_date": "2024-08-16 23:58:36.728000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "fe69cf80-946a-45d6-9fba-55ebfc0f038c", - "volume_id": null - }, - { - "id": "4d797135-f34a-4d10-b1f6-2a3c955bb420", - "created_date": "2024-08-16 23:58:36.674000", - "last_modified_date": "2024-08-16 23:58:36.674000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", - "volume_id": null - }, - { - "id": "4e2f1c24-3219-48f9-a7bb-b7860375d8e1", - "created_date": "2024-08-16 23:58:36.668000", - "last_modified_date": "2024-08-16 23:58:36.668000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "c9d4b26a-8431-4f68-8e7a-b61f2cf24176", - "volume_id": null - }, - { - "id": "4e3cbf54-4374-4cf7-8247-a6457475afb2", - "created_date": "2024-08-16 23:58:36.020000", - "last_modified_date": "2024-08-16 23:58:36.020000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "13", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", - "volume_id": null - }, - { - "id": "4e3fd0ae-4c8f-4a9a-8358-07a993f9938a", - "created_date": "2024-08-16 23:58:36.901000", - "last_modified_date": "2024-08-16 23:58:36.901000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "799309bc-d8d9-4d44-9457-bae7f1e7fcbf", - "volume_id": null - }, - { - "id": "4ed9a189-d00a-465d-a75b-01df03b6d4f1", - "created_date": "2024-08-16 23:58:35.603000", - "last_modified_date": "2024-08-16 23:58:35.603000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "5dbe4d8b-331a-41ad-bcdc-01196dc1d58d", - "volume_id": null - }, - { - "id": "4f31d96b-fd07-4136-94f7-270f571a92d1", - "created_date": "2024-08-16 23:58:36.013000", - "last_modified_date": "2024-08-16 23:58:36.013000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "10", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", - "volume_id": null - }, - { - "id": "4f949f0d-8984-4c31-8143-21fac268f221", - "created_date": "2024-08-16 23:58:36.437000", - "last_modified_date": "2024-08-16 23:58:36.437000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "89", - "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", - "volume_id": null - }, - { - "id": "50870a01-229c-4623-a4aa-c96fe6d6b2b6", - "created_date": "2024-08-16 23:58:36.246000", - "last_modified_date": "2024-08-16 23:58:36.246000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "639eed1d-3ccf-4bfa-a595-06b44a4e5b8f", - "volume_id": null - }, - { - "id": "508b865c-ad22-4046-a6c3-85944ccb738e", - "created_date": "2024-08-16 23:58:35.791000", - "last_modified_date": "2024-08-16 23:58:35.791000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "5a72a63d-8fbd-46a8-b201-9b6e035c782a", - "volume_id": null - }, - { - "id": "50d633f3-56ab-462b-b5a5-5391a1c0a2f9", - "created_date": "2024-08-16 23:58:36.139000", - "last_modified_date": "2024-08-16 23:58:36.139000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "c0543c9f-d712-4dce-9b59-2bf73b800b31", - "volume_id": null - }, - { - "id": "50d8b5fe-46c0-4f0f-b565-1cc77c58f323", - "created_date": "2024-08-16 23:58:36.870000", - "last_modified_date": "2024-08-16 23:58:36.870000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "14", - "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", - "volume_id": null - }, - { - "id": "50e60c40-dddb-4cf8-91cc-c6456ddb1af0", - "created_date": "2024-08-16 23:58:35.640000", - "last_modified_date": "2024-08-16 23:58:35.640000", - "version": 0, - "in_stock": 0, - "is_read": 1, - "issue_number": "11", - "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", - "volume_id": null - }, - { - "id": "5106295e-2a16-4abf-bd36-de422a188c37", - "created_date": "2024-08-16 23:58:35.769000", - "last_modified_date": "2024-08-16 23:58:35.769000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "23", - "comic_id": "031b7570-04bc-4f56-834e-61c5792b6e5e", - "volume_id": null - }, - { - "id": "52f4f703-818b-4d55-accd-8e98645d2c31", - "created_date": "2024-08-16 23:58:35.876000", - "last_modified_date": "2024-08-16 23:58:35.876000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "19", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "53a63366-716b-424c-826e-5dfe80c76b93", - "created_date": "2024-08-16 23:58:36.357000", - "last_modified_date": "2024-08-16 23:58:36.357000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "c91ec109-0b4d-4fd6-995f-1a828958493f", - "volume_id": null - }, - { - "id": "53bdb4d5-ac28-40e7-a8dd-523c326857a0", - "created_date": "2024-08-16 23:58:36.940000", - "last_modified_date": "2024-08-16 23:58:36.940000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "41", - "comic_id": "2a4b287e-4b05-4016-8eb4-21fc225be24d", - "volume_id": null - }, - { - "id": "53c170d0-03aa-486e-bce5-57fa4c0800ea", - "created_date": "2024-08-16 23:58:36.276000", - "last_modified_date": "2024-08-16 23:58:36.276000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "3e96be30-f58f-459d-bb97-cfa574b9487c", - "volume_id": null - }, - { - "id": "53ed5350-0319-4681-9dae-1587f5c3b4fe", - "created_date": "2024-08-16 23:58:35.724000", - "last_modified_date": "2024-08-16 23:58:35.724000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "17", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "53ee8dc8-b313-4f09-90c9-f8522d65cbc7", - "created_date": "2024-08-16 23:58:36.661000", - "last_modified_date": "2024-08-16 23:58:36.661000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "c9d4b26a-8431-4f68-8e7a-b61f2cf24176", - "volume_id": null - }, - { - "id": "553d39ec-626d-4b86-ad60-eedd42fed269", - "created_date": "2024-08-16 23:58:36.834000", - "last_modified_date": "2024-08-16 23:58:36.834000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "877c8105-ea6c-4624-b458-60d18b608c15", - "volume_id": null - }, - { - "id": "553e9d19-2776-4abe-b567-dd7bf8b76f90", - "created_date": "2024-08-16 23:58:36.164000", - "last_modified_date": "2024-08-16 23:58:36.164000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "5fc6600f-b005-4d6b-a1af-a17cc2701d81", - "volume_id": null - }, - { - "id": "558a29b7-53c5-4be3-9bff-4189f6a2fdbd", - "created_date": "2024-08-16 23:58:36.760000", - "last_modified_date": "2024-08-16 23:58:36.760000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", - "volume_id": null - }, - { - "id": "560766b3-be4c-4e73-9c10-568f6cbc9c38", - "created_date": "2024-08-16 23:58:36.697000", - "last_modified_date": "2024-08-16 23:58:36.697000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "151", - "comic_id": "cc96b25e-b827-4ff0-a94a-b82d30ca883c", - "volume_id": null - }, - { - "id": "560b1722-b8bd-41ad-bc9b-85bbd965cd68", - "created_date": "2024-08-16 23:58:36.177000", - "last_modified_date": "2024-08-16 23:58:36.177000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", - "volume_id": null - }, - { - "id": "56a4c920-346a-42af-80e1-921cf249cbfe", - "created_date": "2024-08-16 23:58:35.650000", - "last_modified_date": "2024-08-16 23:58:35.650000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", - "volume_id": null - }, - { - "id": "570c91a5-462c-4280-a42a-f084edf1f69e", - "created_date": "2024-08-16 23:58:37.038000", - "last_modified_date": "2024-08-16 23:58:37.038000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "523", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "5788099a-8fec-4fbe-b656-17af6e96774f", - "created_date": "2024-08-16 23:58:36.679000", - "last_modified_date": "2024-08-16 23:58:36.679000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", - "volume_id": null - }, - { - "id": "57cd70ac-5ca4-4148-9f13-e239b4cf462c", - "created_date": "2024-08-16 23:58:37.030000", - "last_modified_date": "2024-08-16 23:58:37.030000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "518", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "580a1e4f-c0c5-486b-9626-f89daedc18db", - "created_date": "2024-08-16 23:58:36.425000", - "last_modified_date": "2024-08-16 23:58:36.425000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "82", - "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", - "volume_id": null - }, - { - "id": "58194686-7e3d-4089-a936-a26acc5400e5", - "created_date": "2024-08-16 23:58:37.130000", - "last_modified_date": "2024-08-16 23:58:37.130000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "9", - "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", - "volume_id": null - }, - { - "id": "582f3b41-b112-4c2f-a8ff-2704ac7b73c1", - "created_date": "2024-08-16 23:58:36.984000", - "last_modified_date": "2024-08-16 23:58:36.984000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "16", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "584fc3ae-1f83-4966-b4ca-7c9540f318a2", - "created_date": "2024-08-16 23:58:36.156000", - "last_modified_date": "2024-08-16 23:58:36.156000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "5fc6600f-b005-4d6b-a1af-a17cc2701d81", - "volume_id": null - }, - { - "id": "58c2f75e-9ecb-4d2c-8df3-ef37464de177", - "created_date": "2024-08-16 23:58:35.619000", - "last_modified_date": "2024-08-16 23:58:35.619000", - "version": 0, - "in_stock": 0, - "is_read": 1, - "issue_number": "4", - "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", - "volume_id": null - }, - { - "id": "59709e87-af75-4edb-a3eb-2a0e5a567fa6", - "created_date": "2024-08-16 23:58:35.984000", - "last_modified_date": "2024-08-16 23:58:35.984000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "11", - "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", - "volume_id": null - }, - { - "id": "5978e77b-b47e-4068-bc92-454fed22e1aa", - "created_date": "2024-08-16 23:58:36.777000", - "last_modified_date": "2024-08-16 23:58:36.777000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "13", - "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", - "volume_id": null - }, - { - "id": "59d9422f-9145-4150-9d48-fda6f1967829", - "created_date": "2024-08-16 23:58:37.026000", - "last_modified_date": "2024-08-16 23:58:37.026000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "516", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "5a44e6e6-df4b-4c5f-b3ad-bbe65392023f", - "created_date": "2024-08-16 23:58:35.762000", - "last_modified_date": "2024-08-16 23:58:35.762000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "21", - "comic_id": "031b7570-04bc-4f56-834e-61c5792b6e5e", - "volume_id": null - }, - { - "id": "5a90707f-2840-476e-8e58-b1dd3b692c4f", - "created_date": "2024-08-16 23:58:37.052000", - "last_modified_date": "2024-08-16 23:58:37.052000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "358d8ebf-8ada-4bc5-b47a-dd8a3b52b2c3", - "volume_id": null - }, - { - "id": "5b644415-ba50-44cd-abb5-0fa0d269792f", - "created_date": "2024-08-16 23:58:37.024000", - "last_modified_date": "2024-08-16 23:58:37.024000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "515", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "5be277f4-8e84-4bf5-8594-34320a6b9b9a", - "created_date": "2024-08-16 23:58:36.769000", - "last_modified_date": "2024-08-16 23:58:36.769000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "8", - "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", - "volume_id": null - }, - { - "id": "5c38f1c0-abb7-418a-a0d2-d0d89d12838c", - "created_date": "2024-08-16 23:58:36.571000", - "last_modified_date": "2024-08-16 23:58:36.571000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "45bd0c8a-845f-4ab3-9281-c31e5a3d4472", - "volume_id": null - }, - { - "id": "5c39c533-e7db-4dbe-8ad8-d0494dd31cd9", - "created_date": "2024-08-16 23:58:36.835000", - "last_modified_date": "2024-08-16 23:58:36.835000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "39f1943d-0620-4d88-8709-c0bf8f35790d", - "volume_id": null - }, - { - "id": "5c7e39f6-d6e9-427c-a2e1-f98ffe1464cd", - "created_date": "2024-08-16 23:58:36.941000", - "last_modified_date": "2024-08-16 23:58:36.941000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "42", - "comic_id": "2a4b287e-4b05-4016-8eb4-21fc225be24d", - "volume_id": null - }, - { - "id": "5ca1f977-741c-4f1d-be36-e3dfea16057d", - "created_date": "2024-08-16 23:58:37.135000", - "last_modified_date": "2024-08-16 23:58:37.135000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "12", - "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", - "volume_id": null - }, - { - "id": "5d02d869-8104-47af-a5e4-717879057769", - "created_date": "2024-08-16 23:58:36.828000", - "last_modified_date": "2024-08-16 23:58:36.828000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "8e293af3-05c6-4dcc-9cbf-b87512ec975b", - "volume_id": null - }, - { - "id": "5d298a9f-8cce-488d-ba62-eeb64359e9ff", - "created_date": "2024-08-16 23:58:36.473000", - "last_modified_date": "2024-08-16 23:58:36.473000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "32", - "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", - "volume_id": null - }, - { - "id": "5de11b92-cd63-43c4-801f-eaed70a7cb08", - "created_date": "2024-08-16 23:58:36.738000", - "last_modified_date": "2024-08-16 23:58:36.738000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "8690631a-e99c-44be-887c-8fd2bb222ee1", - "volume_id": null - }, - { - "id": "5e1fb9d1-7552-4483-9d3a-8d8b3df1ed2c", - "created_date": "2024-08-16 23:58:36.377000", - "last_modified_date": "2024-08-16 23:58:36.377000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "e048cec2-52b9-48f8-9975-cfe17ed85aae", - "volume_id": null - }, - { - "id": "5e269ee8-33cc-4887-9f08-a57071b43745", - "created_date": "2024-08-16 23:58:36.359000", - "last_modified_date": "2024-08-16 23:58:36.359000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "c91ec109-0b4d-4fd6-995f-1a828958493f", - "volume_id": null - }, - { - "id": "5e377def-5226-4648-a906-bc74e6eaad49", - "created_date": "2024-08-16 23:58:36.933000", - "last_modified_date": "2024-08-16 23:58:36.933000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "571cfe31-a696-41ca-8c28-ac68b17909ff", - "volume_id": null - }, - { - "id": "5e39c9e0-8e08-4b08-854d-3f945298c80b", - "created_date": "2024-08-16 23:58:36.947000", - "last_modified_date": "2024-08-16 23:58:36.947000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "35894d94-1fb5-49de-ba77-cf2626cda939", - "volume_id": null - }, - { - "id": "5e9a7d4b-56e1-4489-9ba5-023f83f41954", - "created_date": "2024-08-16 23:58:37.081000", - "last_modified_date": "2024-08-16 23:58:37.081000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "b4c866cb-9461-4aa4-bc3b-ef3e63848775", - "volume_id": null - }, - { - "id": "5ec5864c-f555-4507-8080-428da4d1a5fc", - "created_date": "2024-08-16 23:58:36.305000", - "last_modified_date": "2024-08-16 23:58:36.305000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "46", - "comic_id": "33b14231-7f52-4a1d-8909-cb00e6a241ac", - "volume_id": null - }, - { - "id": "5ee98778-ef89-4a13-8e25-4c1c46361176", - "created_date": "2024-08-16 23:58:37.033000", - "last_modified_date": "2024-08-16 23:58:37.033000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "520", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "5f0773e2-b042-40a1-b79c-7e2f3de5bd71", - "created_date": "2024-08-16 23:58:36.413000", - "last_modified_date": "2024-08-16 23:58:36.413000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "49", - "comic_id": "57965b27-1330-4921-8c0b-4b09ee06084f", - "volume_id": null - }, - { - "id": "5f2af7c9-e5a3-4d3b-8ec8-3f39d4aa6527", - "created_date": "2024-08-16 23:58:37.056000", - "last_modified_date": "2024-08-16 23:58:37.056000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "db80cac0-8598-4063-9a5f-cd4f9c0f457c", - "volume_id": null - }, - { - "id": "5f2c70b5-49dd-425b-a9d6-0b62fc575741", - "created_date": "2024-08-16 23:58:36.298000", - "last_modified_date": "2024-08-16 23:58:36.298000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "13281ef7-5945-49a9-b8f1-c5e8548c18ec", - "volume_id": null - }, - { - "id": "5f41919f-8b39-4947-adce-ec591232e243", - "created_date": "2024-08-16 23:58:36.873000", - "last_modified_date": "2024-08-16 23:58:36.873000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", - "volume_id": null - }, - { - "id": "5f7a9c3b-3cb5-4ba0-8ce5-7c56860dc912", - "created_date": "2024-08-16 23:58:35.822000", - "last_modified_date": "2024-08-16 23:58:35.822000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "5f8c388e-7c00-4e39-99a6-4419be6cf0ae", - "created_date": "2024-08-16 23:58:35.742000", - "last_modified_date": "2024-08-16 23:58:35.742000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "be424be0-a91c-47b4-b4a9-1e1d760a7177", - "volume_id": null - }, - { - "id": "5fe21289-a683-4264-a4fe-d5c3daecede9", - "created_date": "2024-08-16 23:58:35.827000", - "last_modified_date": "2024-08-16 23:58:35.827000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "60503cd0-1e4a-4b89-b7be-4231dcc34955", - "created_date": "2024-08-16 23:58:36.179000", - "last_modified_date": "2024-08-16 23:58:36.179000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", - "volume_id": null - }, - { - "id": "605bf803-637d-446b-b6dd-69460136361a", - "created_date": "2024-08-16 23:58:35.825000", - "last_modified_date": "2024-08-16 23:58:35.825000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "60e15e0e-fee2-4bb2-a1f5-aee20c522296", - "created_date": "2024-08-16 23:58:37.048000", - "last_modified_date": "2024-08-16 23:58:37.048000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "b9a8abf9-b259-4a6f-be2d-75f211788440", - "volume_id": null - }, - { - "id": "60fe7c05-28a2-428b-b984-87ba19adfda2", - "created_date": "2024-08-16 23:58:36.070000", - "last_modified_date": "2024-08-16 23:58:36.070000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "11", - "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", - "volume_id": null - }, - { - "id": "611618e6-b95c-40d8-8bb9-41381d394936", - "created_date": "2024-08-16 23:58:36.470000", - "last_modified_date": "2024-08-16 23:58:36.470000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "30", - "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", - "volume_id": null - }, - { - "id": "61363eca-f00f-4e5f-b01f-520871ea9cb3", - "created_date": "2024-08-16 23:58:36.099000", - "last_modified_date": "2024-08-16 23:58:36.099000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "12fde8f7-8ce3-4398-8095-5bb9b5d9508d", - "volume_id": null - }, - { - "id": "61bd2225-4a5b-43cc-a45e-7d2d1e5022ae", - "created_date": "2024-08-16 23:58:36.395000", - "last_modified_date": "2024-08-16 23:58:36.395000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "213", - "comic_id": "d24801e2-fbfe-4497-873f-4d8edb182ae4", - "volume_id": null - }, - { - "id": "61cf3739-2608-4cb3-a62c-315e5ce0a698", - "created_date": "2024-08-16 23:58:35.830000", - "last_modified_date": "2024-08-16 23:58:35.830000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "62d6b633-63d3-4ad0-ba57-f9b6e6f60c81", - "created_date": "2024-08-16 23:58:36.435000", - "last_modified_date": "2024-08-16 23:58:36.435000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "88", - "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", - "volume_id": null - }, - { - "id": "62ebb222-591c-4259-b440-2f011bccb889", - "created_date": "2024-08-16 23:58:36.914000", - "last_modified_date": "2024-08-16 23:58:36.914000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "0", - "comic_id": "df47cdb1-0b41-4baa-83ce-fcfbb1c2bd51", - "volume_id": null - }, - { - "id": "6308afaa-46b0-4441-a09b-c60ad5ffcc2f", - "created_date": "2024-08-16 23:58:35.949000", - "last_modified_date": "2024-08-16 23:58:35.949000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "11", - "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", - "volume_id": null - }, - { - "id": "636512ab-1107-4ca9-a138-5098fd8934ab", - "created_date": "2024-08-16 23:58:36.292000", - "last_modified_date": "2024-08-16 23:58:36.292000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "358f99c2-93f2-489d-83a4-03267fad8597", - "volume_id": null - }, - { - "id": "636d5e78-4916-4594-9519-c6294605f08c", - "created_date": "2024-08-16 23:58:35.776000", - "last_modified_date": "2024-08-16 23:58:35.776000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "e4002ab9-0f26-48e6-9927-63c350df8015", - "volume_id": null - }, - { - "id": "638c3f28-7006-45f4-99b0-dde81c2efdce", - "created_date": "2024-08-16 23:58:35.766000", - "last_modified_date": "2024-08-16 23:58:35.766000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "22", - "comic_id": "031b7570-04bc-4f56-834e-61c5792b6e5e", - "volume_id": null - }, - { - "id": "6431421d-4dae-49da-971e-3f6f92d837c1", - "created_date": "2024-08-16 23:58:36.601000", - "last_modified_date": "2024-08-16 23:58:36.601000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "21", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "6587fd05-ef54-4b99-bade-a9116988cda1", - "created_date": "2024-08-16 23:58:36.041000", - "last_modified_date": "2024-08-16 23:58:36.041000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "bf23d317-f5b6-4cf8-8a05-4397888a82c9", - "volume_id": null - }, - { - "id": "65dad05f-471c-4b00-bff8-833c0dd1d47e", - "created_date": "2024-08-16 23:58:36.110000", - "last_modified_date": "2024-08-16 23:58:36.110000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "0f0bfd13-f6f0-436c-ad74-5e40ea6a0cf8", - "volume_id": null - }, - { - "id": "65f0f591-904e-487b-adaa-b2cd475241ce", - "created_date": "2024-08-16 23:58:37.058000", - "last_modified_date": "2024-08-16 23:58:37.058000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "17", - "comic_id": "6fd2c6fd-f8f1-4f13-892d-887b315fa4a3", - "volume_id": null - }, - { - "id": "66297d4f-a22a-4637-bce4-b9f2f3a12921", - "created_date": "2024-08-16 23:58:35.690000", - "last_modified_date": "2024-08-16 23:58:35.690000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "665530c9-d084-43fa-8524-4c9df79d2482", - "created_date": "2024-08-16 23:58:36.982000", - "last_modified_date": "2024-08-16 23:58:36.982000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "15", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "66703810-7e60-41cf-a542-dbdc03e0cd26", - "created_date": "2024-08-16 23:58:35.879000", - "last_modified_date": "2024-08-16 23:58:35.879000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "20", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "667b8ef3-2420-45c3-9a86-cd0baf27f857", - "created_date": "2024-08-16 23:58:37.111000", - "last_modified_date": "2024-08-16 23:58:37.111000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "2ab84282-d7f5-43ef-af41-df0f756a62f7", - "volume_id": null - }, - { - "id": "6788e63c-ac05-4dfd-9291-f301b075c2d6", - "created_date": "2024-08-16 23:58:35.921000", - "last_modified_date": "2024-08-16 23:58:35.921000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", - "volume_id": null - }, - { - "id": "67ca9f0c-43bf-44be-825f-afd7752f566b", - "created_date": "2024-08-16 23:58:36.868000", - "last_modified_date": "2024-08-16 23:58:36.868000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "13", - "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", - "volume_id": null - }, - { - "id": "680cce2b-115b-4d65-8b49-1bdbd8c16db4", - "created_date": "2024-08-16 23:58:35.989000", - "last_modified_date": "2024-08-16 23:58:35.989000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", - "volume_id": null - }, - { - "id": "6838063d-f9bd-4511-9dcb-fe9afc89ec63", - "created_date": "2024-08-16 23:58:36.232000", - "last_modified_date": "2024-08-16 23:58:36.232000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "ff81bcf6-b368-4264-872c-544e85ec80e8", - "volume_id": null - }, - { - "id": "6919e636-10a4-4245-8fef-7e300a496656", - "created_date": "2024-08-16 23:58:36.398000", - "last_modified_date": "2024-08-16 23:58:36.398000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "215", - "comic_id": "d24801e2-fbfe-4497-873f-4d8edb182ae4", - "volume_id": null - }, - { - "id": "69afd474-5516-49c9-b8b3-9392f7a97bbb", - "created_date": "2024-08-16 23:58:36.795000", - "last_modified_date": "2024-08-16 23:58:36.795000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "9", - "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", - "volume_id": null - }, - { - "id": "6a22c937-eeb8-4e13-9560-b9afc0e216e3", - "created_date": "2024-08-16 23:58:36.208000", - "last_modified_date": "2024-08-16 23:58:36.208000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", - "volume_id": null - }, - { - "id": "6a8e02fc-90f0-4379-a460-6cb91b7c36b8", - "created_date": "2024-08-16 23:58:35.934000", - "last_modified_date": "2024-08-16 23:58:35.934000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", - "volume_id": null - }, - { - "id": "6b0b845b-bf0f-487e-9f60-bcc97e64038d", - "created_date": "2024-08-16 23:58:36.033000", - "last_modified_date": "2024-08-16 23:58:36.033000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "bf23d317-f5b6-4cf8-8a05-4397888a82c9", - "volume_id": null - }, - { - "id": "6b430f58-0256-4f97-8d47-b7dca655ffca", - "created_date": "2024-08-16 23:58:35.746000", - "last_modified_date": "2024-08-16 23:58:35.746000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "be424be0-a91c-47b4-b4a9-1e1d760a7177", - "volume_id": null - }, - { - "id": "6b64dfb3-d1cd-49f6-b3fe-53eb912b9978", - "created_date": "2024-08-16 23:58:35.606000", - "last_modified_date": "2024-08-16 23:58:35.606000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "5dbe4d8b-331a-41ad-bcdc-01196dc1d58d", - "volume_id": null - }, - { - "id": "6c600589-5aae-4eb8-ae34-981b35a16853", - "created_date": "2024-08-16 23:58:35.963000", - "last_modified_date": "2024-08-16 23:58:35.963000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", - "volume_id": null - }, - { - "id": "6cc634ca-8a6a-4f0f-8984-c6ef3fbd1d62", - "created_date": "2024-08-16 23:58:37.125000", - "last_modified_date": "2024-08-16 23:58:37.125000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", - "volume_id": null - }, - { - "id": "6cff1d7a-89f7-4f3f-a28d-1419557fab2c", - "created_date": "2024-08-16 23:58:36.440000", - "last_modified_date": "2024-08-16 23:58:36.440000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "91", - "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", - "volume_id": null - }, - { - "id": "6de115d6-bde2-47ef-84d2-02ee3396c594", - "created_date": "2024-08-16 23:58:36.726000", - "last_modified_date": "2024-08-16 23:58:36.726000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "25841b05-c246-4484-9ef2-71f8dcdb39ed", - "volume_id": null - }, - { - "id": "6dfaea42-c371-409b-bd54-4b866b9490c0", - "created_date": "2024-08-16 23:58:36.011000", - "last_modified_date": "2024-08-16 23:58:36.011000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "9", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", - "volume_id": null - }, - { - "id": "6e100dcb-0156-4c03-afb2-d7021bdceda0", - "created_date": "2024-08-16 23:58:35.701000", - "last_modified_date": "2024-08-16 23:58:35.701000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "9", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "6e1a2ef5-25cf-4b51-834f-a0b66861b5cb", - "created_date": "2024-08-16 23:58:37.018000", - "last_modified_date": "2024-08-16 23:58:37.018000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "511", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "6e2de560-8881-40a6-99dc-daaed77793e3", - "created_date": "2024-08-16 23:58:36.052000", - "last_modified_date": "2024-08-16 23:58:36.052000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", - "volume_id": null - }, - { - "id": "6e975aae-0d92-43c5-910e-ff450fa28490", - "created_date": "2024-08-16 23:58:35.918000", - "last_modified_date": "2024-08-16 23:58:35.918000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", - "volume_id": null - }, - { - "id": "6f0e6451-4401-466f-8fb4-41f37fd5b8df", - "created_date": "2024-08-16 23:58:35.854000", - "last_modified_date": "2024-08-16 23:58:35.854000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "12", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "6f83f43d-0064-4b84-a5a2-ef553b05ced3", - "created_date": "2024-08-16 23:58:37.065000", - "last_modified_date": "2024-08-16 23:58:37.065000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "21", - "comic_id": "6fd2c6fd-f8f1-4f13-892d-887b315fa4a3", - "volume_id": null - }, - { - "id": "6f856050-3f81-48a1-8547-1a9331f247e5", - "created_date": "2024-08-16 23:58:36.422000", - "last_modified_date": "2024-08-16 23:58:36.422000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "52f0849c-3c1c-49a0-9448-8b5b2c9b0c0c", - "volume_id": null - }, - { - "id": "6fff0fc9-6e6c-4220-8501-cba205822252", - "created_date": "2024-08-16 23:58:36.089000", - "last_modified_date": "2024-08-16 23:58:36.089000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "11bc20d5-bfeb-4825-9e0f-3d6954020b07", - "volume_id": null - }, - { - "id": "704757ab-0081-428f-8de7-d5b44fcd7142", - "created_date": "2024-08-16 23:58:36.998000", - "last_modified_date": "2024-08-16 23:58:36.998000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "851bc748-2602-4f20-826f-a59a7087d11f", - "volume_id": null - }, - { - "id": "70517bb0-e122-4875-97d1-ae1ca1c9f317", - "created_date": "2024-08-16 23:58:36.353000", - "last_modified_date": "2024-08-16 23:58:36.353000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "12", - "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", - "volume_id": null - }, - { - "id": "7082f983-0a03-475c-a94f-49b3ff7dda07", - "created_date": "2024-08-16 23:58:36.981000", - "last_modified_date": "2024-08-16 23:58:36.981000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "14", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "709f7ff9-eba5-4e9e-8d01-ea052ce94b91", - "created_date": "2024-08-16 23:58:36.274000", - "last_modified_date": "2024-08-16 23:58:36.274000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "19", - "comic_id": "0cb7d5d0-4204-41a4-8698-b8dd56f1c597", - "volume_id": null - }, - { - "id": "70e02b19-4b36-4ffe-bfba-17a36a125edf", - "created_date": "2024-08-16 23:58:36.281000", - "last_modified_date": "2024-08-16 23:58:36.281000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "3e96be30-f58f-459d-bb97-cfa574b9487c", - "volume_id": null - }, - { - "id": "7171d6d9-0799-44df-a592-6730f1790c0c", - "created_date": "2024-08-16 23:58:36.286000", - "last_modified_date": "2024-08-16 23:58:36.286000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "45f57e1d-f3aa-40f7-b89a-b02eeda10577", - "volume_id": null - }, - { - "id": "71b33e1c-8d1b-4e16-abdb-00a292c352f5", - "created_date": "2024-08-16 23:58:36.173000", - "last_modified_date": "2024-08-16 23:58:36.173000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", - "volume_id": null - }, - { - "id": "72047b9a-6eb7-4886-8133-1f68d3214d01", - "created_date": "2024-08-16 23:58:37.108000", - "last_modified_date": "2024-08-16 23:58:37.108000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "639f7e71-1012-49a6-bc3a-1ac7b1de3084", - "volume_id": null - }, - { - "id": "72480cf6-8f35-48e8-82bb-6808c589cd37", - "created_date": "2024-08-16 23:58:36.337000", - "last_modified_date": "2024-08-16 23:58:36.337000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", - "volume_id": null - }, - { - "id": "728be618-1a83-4a0e-b45c-1f60ee210047", - "created_date": "2024-08-16 23:58:37.102000", - "last_modified_date": "2024-08-16 23:58:37.102000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "07574acf-8f77-4da7-87fd-c49f40536baf", - "volume_id": null - }, - { - "id": "72ecd5b2-d918-4d3b-bfce-7526c292afa9", - "created_date": "2024-08-16 23:58:36.270000", - "last_modified_date": "2024-08-16 23:58:36.270000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "5d0fd720-7875-4f9f-86eb-07ee0723908f", - "volume_id": null - }, - { - "id": "737a78cd-9ff9-4aa9-813f-ef67fbaa00ee", - "created_date": "2024-08-16 23:58:36.659000", - "last_modified_date": "2024-08-16 23:58:36.659000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "112", - "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", - "volume_id": null - }, - { - "id": "737b85ee-60e5-4cd6-ace4-8ee1fddba69b", - "created_date": "2024-08-16 23:58:37.060000", - "last_modified_date": "2024-08-16 23:58:37.060000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "18", - "comic_id": "6fd2c6fd-f8f1-4f13-892d-887b315fa4a3", - "volume_id": null - }, - { - "id": "73b81180-54f5-4937-8d1e-5d61fe7498e8", - "created_date": "2024-08-16 23:58:36.893000", - "last_modified_date": "2024-08-16 23:58:36.893000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "0fd9969e-8e1f-4f42-94ac-fab4d2d614c2", - "volume_id": null - }, - { - "id": "73be0679-196f-4739-a54f-8a83d5e6906d", - "created_date": "2024-08-16 23:58:36.319000", - "last_modified_date": "2024-08-16 23:58:36.319000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "7c110b15-bbfc-472e-830b-b8db6ddb274e", - "volume_id": null - }, - { - "id": "73c52885-1826-4941-8144-1f3dfbafc0de", - "created_date": "2024-08-16 23:58:35.795000", - "last_modified_date": "2024-08-16 23:58:35.795000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "b51c738d-8ba1-4451-8f89-f49c1968ac42", - "volume_id": null - }, - { - "id": "74dbdadb-fa8d-4e49-bf9a-067cc81a2894", - "created_date": "2024-08-16 23:58:36.384000", - "last_modified_date": "2024-08-16 23:58:36.384000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "207", - "comic_id": "d24801e2-fbfe-4497-873f-4d8edb182ae4", - "volume_id": null - }, - { - "id": "751c3734-0a8a-4fa4-adfe-7f3729025ecb", - "created_date": "2024-08-16 23:58:35.730000", - "last_modified_date": "2024-08-16 23:58:35.730000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "19", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "75355c6b-eda6-411d-bf85-cda0a12f1a02", - "created_date": "2024-08-16 23:58:35.712000", - "last_modified_date": "2024-08-16 23:58:35.712000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "13", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "75433447-644f-4a4f-a5d7-83d123b4daa1", - "created_date": "2024-08-16 23:58:36.670000", - "last_modified_date": "2024-08-16 23:58:36.670000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", - "volume_id": null - }, - { - "id": "76ee9751-50ba-4600-8450-2ef9d537b882", - "created_date": "2024-08-16 23:58:36.004000", - "last_modified_date": "2024-08-16 23:58:36.004000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", - "volume_id": null - }, - { - "id": "77f5cdc7-0db2-4aa6-829b-9f6606408112", - "created_date": "2024-08-16 23:58:36.788000", - "last_modified_date": "2024-08-16 23:58:36.788000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", - "volume_id": null - }, - { - "id": "7884bbf8-53c1-4ac2-8801-df5ef9b29bc2", - "created_date": "2024-08-16 23:58:37.075000", - "last_modified_date": "2024-08-16 23:58:37.075000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "231e65eb-eecb-4946-a643-6184b767e321", - "volume_id": null - }, - { - "id": "79468348-8896-4ff2-b36f-da594eaa78e4", - "created_date": "2024-08-16 23:58:36.671000", - "last_modified_date": "2024-08-16 23:58:36.671000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", - "volume_id": null - }, - { - "id": "79704f8d-ca6a-4eee-acb4-bebbd2695f19", - "created_date": "2024-08-16 23:58:36.809000", - "last_modified_date": "2024-08-16 23:58:36.809000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "f4f227f0-a6d1-49fd-baea-6791245a89e7", - "volume_id": null - }, - { - "id": "79928b2c-4d50-49af-b8a8-6434359119af", - "created_date": "2024-08-16 23:58:36.541000", - "last_modified_date": "2024-08-16 23:58:36.541000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "f4cb5b24-00ea-4249-9d09-45432c168b8f", - "volume_id": null - }, - { - "id": "79ca675a-0a4f-4109-a520-65a5b5fbd0ca", - "created_date": "2024-08-16 23:58:36.468000", - "last_modified_date": "2024-08-16 23:58:36.468000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "29", - "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", - "volume_id": null - }, - { - "id": "7a628ecb-48f0-4627-ba08-f5342160b4ce", - "created_date": "2024-08-16 23:58:36.159000", - "last_modified_date": "2024-08-16 23:58:36.159000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "5fc6600f-b005-4d6b-a1af-a17cc2701d81", - "volume_id": null - }, - { - "id": "7abdddc6-f990-4eb1-8e2c-5ef97972f451", - "created_date": "2024-08-16 23:58:36.204000", - "last_modified_date": "2024-08-16 23:58:36.204000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", - "volume_id": null - }, - { - "id": "7ac72c94-7517-401b-84f6-a79829e69ed2", - "created_date": "2024-08-16 23:58:36.975000", - "last_modified_date": "2024-08-16 23:58:36.975000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "11", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "7ae8e72f-fb2a-4ef7-84e5-a1e6dd99d9d9", - "created_date": "2024-08-16 23:58:35.670000", - "last_modified_date": "2024-08-16 23:58:35.670000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "8", - "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", - "volume_id": null - }, - { - "id": "7b7a972c-fbb7-4559-a2df-ccdd899840cc", - "created_date": "2024-08-16 23:58:36.386000", - "last_modified_date": "2024-08-16 23:58:36.386000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "208", - "comic_id": "d24801e2-fbfe-4497-873f-4d8edb182ae4", - "volume_id": null - }, - { - "id": "7c195d1b-e0bd-4a5c-a0b9-b3b3c8892e8d", - "created_date": "2024-08-16 23:58:37.036000", - "last_modified_date": "2024-08-16 23:58:37.036000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "522", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "7c38db11-e097-48cc-85f4-fd36e25a2712", - "created_date": "2024-08-16 23:58:36.810000", - "last_modified_date": "2024-08-16 23:58:36.810000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "f4f227f0-a6d1-49fd-baea-6791245a89e7", - "volume_id": null - }, - { - "id": "7c9626a7-3508-4ab1-a225-8451db16502e", - "created_date": "2024-08-16 23:58:36.048000", - "last_modified_date": "2024-08-16 23:58:36.048000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", - "volume_id": null - }, - { - "id": "7cbf9370-bf97-44e2-b711-fdd785869a9b", - "created_date": "2024-08-16 23:58:35.842000", - "last_modified_date": "2024-08-16 23:58:35.842000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "8", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "7e2de3c0-3003-4b56-8ac9-6800ef97d396", - "created_date": "2024-08-16 23:58:36.908000", - "last_modified_date": "2024-08-16 23:58:36.908000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "9aac6484-4c06-4286-815a-219aad25cc74", - "volume_id": null - }, - { - "id": "7f360381-97a4-4569-8fb0-b717a4f9ddeb", - "created_date": "2024-08-16 23:58:36.780000", - "last_modified_date": "2024-08-16 23:58:36.780000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1/2", - "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", - "volume_id": null - }, - { - "id": "7f548d49-fbbe-40bf-8ee1-2a30d58dd2f3", - "created_date": "2024-08-16 23:58:36.210000", - "last_modified_date": "2024-08-16 23:58:36.210000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "8", - "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", - "volume_id": null - }, - { - "id": "7f8b3fd7-db82-427d-a717-0423a2d7fe46", - "created_date": "2024-08-16 23:58:36.529000", - "last_modified_date": "2024-08-16 23:58:36.529000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "2b2a07ba-c5a8-4cb8-b170-990cb941315a", - "volume_id": null - }, - { - "id": "7f9b94ae-2631-4801-956f-53358b878634", - "created_date": "2024-08-16 23:58:36.888000", - "last_modified_date": "2024-08-16 23:58:36.888000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "11", - "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", - "volume_id": null - }, - { - "id": "7fe8b5e2-5b6f-4231-b82c-2946e8930254", - "created_date": "2024-08-16 23:58:36.043000", - "last_modified_date": "2024-08-16 23:58:36.043000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "bf23d317-f5b6-4cf8-8a05-4397888a82c9", - "volume_id": null - }, - { - "id": "800235c2-e5af-4bee-8984-582b495338fd", - "created_date": "2024-08-16 23:58:36.102000", - "last_modified_date": "2024-08-16 23:58:36.102000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "9b924cdc-8959-41e0-a84d-f3e61bbeac44", - "volume_id": null - }, - { - "id": "80934009-0562-408a-8578-cede068974f0", - "created_date": "2024-08-16 23:58:35.901000", - "last_modified_date": "2024-08-16 23:58:35.901000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "b73e5e2f-60e4-42fe-94bf-44fe3755b8b8", - "volume_id": null - }, - { - "id": "809b12a7-e2ed-429a-b14f-a8d4eef6fb13", - "created_date": "2024-08-16 23:58:36.466000", - "last_modified_date": "2024-08-16 23:58:36.466000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "28", - "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", - "volume_id": null - }, - { - "id": "815bfce1-94fb-4867-a392-282722121559", - "created_date": "2024-08-16 23:58:36.793000", - "last_modified_date": "2024-08-16 23:58:36.793000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "8", - "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", - "volume_id": null - }, - { - "id": "817e975e-29e1-4632-b1ba-23c746a91cdc", - "created_date": "2024-08-16 23:58:36.341000", - "last_modified_date": "2024-08-16 23:58:36.341000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", - "volume_id": null - }, - { - "id": "818e9b48-8bad-49f0-910f-d7eb5349f272", - "created_date": "2024-08-16 23:58:35.754000", - "last_modified_date": "2024-08-16 23:58:35.754000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", - "volume_id": null - }, - { - "id": "81fb3fa2-ed65-4122-9418-3772e8abe8a1", - "created_date": "2024-08-16 23:58:36.521000", - "last_modified_date": "2024-08-16 23:58:36.521000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "23", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "823239ed-b2d6-4d52-ad93-784c53a63494", - "created_date": "2024-08-16 23:58:36.515000", - "last_modified_date": "2024-08-16 23:58:36.515000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "21", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "826bf417-ca46-4036-af69-3758ad5c88d4", - "created_date": "2024-08-16 23:58:36.464000", - "last_modified_date": "2024-08-16 23:58:36.464000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "27", - "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", - "volume_id": null - }, - { - "id": "8282ffb7-51d3-4c18-951f-2d0062e285b1", - "created_date": "2024-08-16 23:58:36.576000", - "last_modified_date": "2024-08-16 23:58:36.576000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "0095a769-e5e2-441e-8a38-f98743ee9529", - "volume_id": null - }, - { - "id": "82898012-4e4a-49f4-b725-74928ee0b5bf", - "created_date": "2024-08-16 23:58:36.651000", - "last_modified_date": "2024-08-16 23:58:36.651000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "108", - "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", - "volume_id": null - }, - { - "id": "82ae6cab-0f04-4ea5-ab90-b90d9b24f67d", - "created_date": "2024-08-16 23:58:36.634000", - "last_modified_date": "2024-08-16 23:58:36.634000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "0e6688d2-f347-4800-83b1-1f094b658084", - "volume_id": null - }, - { - "id": "83214bfd-008c-4740-9ec0-99cb95c33ef1", - "created_date": "2024-08-16 23:58:36.339000", - "last_modified_date": "2024-08-16 23:58:36.339000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", - "volume_id": null - }, - { - "id": "837d2d57-2f7d-4754-b4dc-83111703cad5", - "created_date": "2024-08-16 23:58:36.188000", - "last_modified_date": "2024-08-16 23:58:36.188000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "11", - "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", - "volume_id": null - }, - { - "id": "83b61494-58c4-456f-913d-66c41b578ea4", - "created_date": "2024-08-16 23:58:35.680000", - "last_modified_date": "2024-08-16 23:58:35.680000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "11", - "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", - "volume_id": null - }, - { - "id": "84726c1a-2ca0-4dba-89d5-8fea8eb05a28", - "created_date": "2024-08-16 23:58:36.424000", - "last_modified_date": "2024-08-16 23:58:36.424000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "81", - "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", - "volume_id": null - }, - { - "id": "849c5632-21b4-417a-aa3c-c6c1db71fb8a", - "created_date": "2024-08-16 23:58:36.916000", - "last_modified_date": "2024-08-16 23:58:36.916000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "df47cdb1-0b41-4baa-83ce-fcfbb1c2bd51", - "volume_id": null - }, - { - "id": "849e2544-bb49-4c8a-be4d-f4718d80c824", - "created_date": "2024-08-16 23:58:36.681000", - "last_modified_date": "2024-08-16 23:58:36.681000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "8", - "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", - "volume_id": null - }, - { - "id": "84dffba2-3e9f-4ff3-a386-96f812205343", - "created_date": "2024-08-16 23:58:36.430000", - "last_modified_date": "2024-08-16 23:58:36.430000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "85", - "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", - "volume_id": null - }, - { - "id": "858f9475-aaf8-4fbc-9018-396e751acfbd", - "created_date": "2024-08-16 23:58:36.693000", - "last_modified_date": "2024-08-16 23:58:36.693000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "15", - "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", - "volume_id": null - }, - { - "id": "871de10b-67a2-4aa0-8763-16a3d50a9343", - "created_date": "2024-08-16 23:58:36.997000", - "last_modified_date": "2024-08-16 23:58:36.997000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "24", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "873557a0-c166-4d03-a009-52fa0c89b016", - "created_date": "2024-08-16 23:58:36.393000", - "last_modified_date": "2024-08-16 23:58:36.393000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "212", - "comic_id": "d24801e2-fbfe-4497-873f-4d8edb182ae4", - "volume_id": null - }, - { - "id": "8756bfb3-440f-48bd-915c-23567ecc4698", - "created_date": "2024-08-16 23:58:36.845000", - "last_modified_date": "2024-08-16 23:58:36.845000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", - "volume_id": null - }, - { - "id": "87eee47e-e934-46c2-b136-db47886d676c", - "created_date": "2024-08-16 23:58:35.857000", - "last_modified_date": "2024-08-16 23:58:35.857000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "13", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "88339a4a-0465-41e0-917a-93f2bcd5310f", - "created_date": "2024-08-16 23:58:37.032000", - "last_modified_date": "2024-08-16 23:58:37.032000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "519", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "8913b527-3126-42cc-8396-e62067b2b779", - "created_date": "2024-08-16 23:58:36.635000", - "last_modified_date": "2024-08-16 23:58:36.635000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "0e6688d2-f347-4800-83b1-1f094b658084", - "volume_id": null - }, - { - "id": "89acf4ef-090f-4d7c-b9c4-e50100660cf3", - "created_date": "2024-08-16 23:58:36.707000", - "last_modified_date": "2024-08-16 23:58:36.707000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "a2409ef1-82c3-45d1-9c65-b8806a31e525", - "volume_id": null - }, - { - "id": "8a69689b-5e04-425e-b8df-ccc556ddcf93", - "created_date": "2024-08-16 23:58:36.065000", - "last_modified_date": "2024-08-16 23:58:36.065000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "9", - "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", - "volume_id": null - }, - { - "id": "8abf912d-b4d6-46d2-9c9d-85a83f7841db", - "created_date": "2024-08-16 23:58:36.009000", - "last_modified_date": "2024-08-16 23:58:36.009000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "8", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", - "volume_id": null - }, - { - "id": "8ae81ee3-91b3-44f8-8a3a-07255d334a11", - "created_date": "2024-08-16 23:58:36.257000", - "last_modified_date": "2024-08-16 23:58:36.257000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "5d0fd720-7875-4f9f-86eb-07ee0723908f", - "volume_id": null - }, - { - "id": "8ae84cc4-989e-41ab-9280-4ad32b2715a3", - "created_date": "2024-08-16 23:58:36.167000", - "last_modified_date": "2024-08-16 23:58:36.167000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "8", - "comic_id": "5fc6600f-b005-4d6b-a1af-a17cc2701d81", - "volume_id": null - }, - { - "id": "8bbe09d6-1810-41b1-911a-16c894dde7f6", - "created_date": "2024-08-16 23:58:36.540000", - "last_modified_date": "2024-08-16 23:58:36.540000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "f4cb5b24-00ea-4249-9d09-45432c168b8f", - "volume_id": null - }, - { - "id": "8be1a3b8-53d3-4a22-aa6e-71623ed1c2a8", - "created_date": "2024-08-16 23:58:36.404000", - "last_modified_date": "2024-08-16 23:58:36.404000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "20", - "comic_id": "ed58b16e-0701-4373-befe-39118bc2d4cb", - "volume_id": null - }, - { - "id": "8c10dd1b-5020-4210-a64d-44f259a416a3", - "created_date": "2024-08-16 23:58:37.067000", - "last_modified_date": "2024-08-16 23:58:37.067000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "22", - "comic_id": "6fd2c6fd-f8f1-4f13-892d-887b315fa4a3", - "volume_id": null - }, - { - "id": "8c7dea9b-82ae-499d-a482-dc78450db8b0", - "created_date": "2024-08-16 23:58:35.626000", - "last_modified_date": "2024-08-16 23:58:35.626000", - "version": 0, - "in_stock": 0, - "is_read": 1, - "issue_number": "6", - "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", - "volume_id": null - }, - { - "id": "8ce9347d-2b99-4603-a47e-52e0d23b88bc", - "created_date": "2024-08-16 23:58:36.283000", - "last_modified_date": "2024-08-16 23:58:36.283000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "3e96be30-f58f-459d-bb97-cfa574b9487c", - "volume_id": null - }, - { - "id": "8d5c0058-9d4d-4118-b15a-3250da1a1ecb", - "created_date": "2024-08-16 23:58:36.723000", - "last_modified_date": "2024-08-16 23:58:36.723000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "25841b05-c246-4484-9ef2-71f8dcdb39ed", - "volume_id": null - }, - { - "id": "8dcbd7e9-3f36-4953-aed9-c5fd160e49a1", - "created_date": "2024-08-16 23:58:36.162000", - "last_modified_date": "2024-08-16 23:58:36.162000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "5fc6600f-b005-4d6b-a1af-a17cc2701d81", - "volume_id": null - }, - { - "id": "8e16df26-2f24-4c4b-a4a0-1f8a7897f106", - "created_date": "2024-08-16 23:58:36.116000", - "last_modified_date": "2024-08-16 23:58:36.116000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "0f0bfd13-f6f0-436c-ad74-5e40ea6a0cf8", - "volume_id": null - }, - { - "id": "8f58929a-b9af-4833-b7fd-fdff8845a15c", - "created_date": "2024-08-16 23:58:35.644000", - "last_modified_date": "2024-08-16 23:58:35.644000", - "version": 0, - "in_stock": 0, - "is_read": 1, - "issue_number": "12", - "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", - "volume_id": null - }, - { - "id": "8f6eff88-05dc-4ff6-a270-f557b00cbb31", - "created_date": "2024-08-16 23:58:36.730000", - "last_modified_date": "2024-08-16 23:58:36.730000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "fe69cf80-946a-45d6-9fba-55ebfc0f038c", - "volume_id": null - }, - { - "id": "8fe1045a-1180-4a96-9eb8-b8ea14a62e2e", - "created_date": "2024-08-16 23:58:36.813000", - "last_modified_date": "2024-08-16 23:58:36.813000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "97685762-f1ae-4588-913f-0bdc38365360", - "volume_id": null - }, - { - "id": "9076e802-b585-476e-8939-df4706c9a7ca", - "created_date": "2024-08-16 23:58:36.768000", - "last_modified_date": "2024-08-16 23:58:36.768000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", - "volume_id": null - }, - { - "id": "9093f77d-ba1f-417e-be72-d0699e984ccb", - "created_date": "2024-08-16 23:58:36.543000", - "last_modified_date": "2024-08-16 23:58:36.543000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "bf59f643-455e-4b60-95b8-719d55437474", - "volume_id": null - }, - { - "id": "91546e89-ff1e-4caa-82be-4a995251047f", - "created_date": "2024-08-16 23:58:36.530000", - "last_modified_date": "2024-08-16 23:58:36.530000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "2b2a07ba-c5a8-4cb8-b170-990cb941315a", - "volume_id": null - }, - { - "id": "91b15408-4016-484f-bfa6-ae3109c41a3d", - "created_date": "2024-08-16 23:58:36.969000", - "last_modified_date": "2024-08-16 23:58:36.969000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "92360fa2-36ce-46cc-a1a2-528d1537ee52", - "created_date": "2024-08-16 23:58:36.217000", - "last_modified_date": "2024-08-16 23:58:36.217000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "11", - "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", - "volume_id": null - }, - { - "id": "929054d0-5dd2-42b7-8b02-583e2f8c142e", - "created_date": "2024-08-16 23:58:36.762000", - "last_modified_date": "2024-08-16 23:58:36.762000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", - "volume_id": null - }, - { - "id": "9295e4cb-61e1-4875-918e-561a4cb2bb74", - "created_date": "2024-08-16 23:58:36.617000", - "last_modified_date": "2024-08-16 23:58:36.617000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "31", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "9359a0ef-7633-46d0-a570-76adf0e0764a", - "created_date": "2024-08-16 23:58:36.812000", - "last_modified_date": "2024-08-16 23:58:36.812000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "f4f227f0-a6d1-49fd-baea-6791245a89e7", - "volume_id": null - }, - { - "id": "946ead50-ed24-4bb8-9756-fecb4c3c497f", - "created_date": "2024-08-16 23:58:36.001000", - "last_modified_date": "2024-08-16 23:58:36.001000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", - "volume_id": null - }, - { - "id": "956bdb4b-1477-4c74-8d90-77382ce6697c", - "created_date": "2024-08-16 23:58:36.184000", - "last_modified_date": "2024-08-16 23:58:36.184000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "9", - "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", - "volume_id": null - }, - { - "id": "95790d47-1e33-4be6-82bc-2fcd15d1160b", - "created_date": "2024-08-16 23:58:35.961000", - "last_modified_date": "2024-08-16 23:58:35.961000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "15", - "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", - "volume_id": null - }, - { - "id": "964b9724-cb11-4d41-a0b1-40ed37a9c4a7", - "created_date": "2024-08-16 23:58:36.109000", - "last_modified_date": "2024-08-16 23:58:36.109000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "9b924cdc-8959-41e0-a84d-f3e61bbeac44", - "volume_id": null - }, - { - "id": "966895fc-79a9-4e22-ad20-50ce89cfc438", - "created_date": "2024-08-16 23:58:37.071000", - "last_modified_date": "2024-08-16 23:58:37.071000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "23", - "comic_id": "6fd2c6fd-f8f1-4f13-892d-887b315fa4a3", - "volume_id": null - }, - { - "id": "96c09ec0-eb4d-4ee0-a8ef-924fb48173a0", - "created_date": "2024-08-16 23:58:36.144000", - "last_modified_date": "2024-08-16 23:58:36.144000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "c0543c9f-d712-4dce-9b59-2bf73b800b31", - "volume_id": null - }, - { - "id": "96eb1c84-799c-4153-b28a-df27af780653", - "created_date": "2024-08-16 23:58:36.416000", - "last_modified_date": "2024-08-16 23:58:36.416000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "ab9aa494-b791-498d-bf13-6c3c50c16667", - "volume_id": null - }, - { - "id": "97ac080a-8355-4688-9c61-306914dac18e", - "created_date": "2024-08-16 23:58:36.653000", - "last_modified_date": "2024-08-16 23:58:36.653000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "109", - "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", - "volume_id": null - }, - { - "id": "98637c7e-90a7-43ba-b95c-ace797117f60", - "created_date": "2024-08-16 23:58:36.364000", - "last_modified_date": "2024-08-16 23:58:36.364000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "c91ec109-0b4d-4fd6-995f-1a828958493f", - "volume_id": null - }, - { - "id": "9981ba1b-7a04-4b67-bc2a-44ed25fafa1a", - "created_date": "2024-08-16 23:58:36.602000", - "last_modified_date": "2024-08-16 23:58:36.602000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "22", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "99a06cad-625a-4b6e-a491-4951f3a1b929", - "created_date": "2024-08-16 23:58:36.655000", - "last_modified_date": "2024-08-16 23:58:36.655000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "110", - "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", - "volume_id": null - }, - { - "id": "99ac1698-3760-4aac-9c8a-41ae301eb963", - "created_date": "2024-08-16 23:58:37.118000", - "last_modified_date": "2024-08-16 23:58:37.118000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "6d3e9abd-1c42-4024-903f-6b139571da25", - "volume_id": null - }, - { - "id": "9b02efc5-0dcc-461b-b3e8-d48500f7a7b3", - "created_date": "2024-08-16 23:58:36.930000", - "last_modified_date": "2024-08-16 23:58:36.930000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "fa203e3e-db4a-44ed-beab-df526fec838c", - "volume_id": null - }, - { - "id": "9b237640-e735-4e3f-99cc-35f5bee0dbcd", - "created_date": "2024-08-16 23:58:35.978000", - "last_modified_date": "2024-08-16 23:58:35.978000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "8", - "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", - "volume_id": null - }, - { - "id": "9b3b14a3-8b8d-4283-b4ff-c15dcb3e237b", - "created_date": "2024-08-16 23:58:35.682000", - "last_modified_date": "2024-08-16 23:58:35.682000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "9c14793e-c4d9-43fd-a81e-f75345990b8a", - "created_date": "2024-08-16 23:58:36.255000", - "last_modified_date": "2024-08-16 23:58:36.255000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "639eed1d-3ccf-4bfa-a595-06b44a4e5b8f", - "volume_id": null - }, - { - "id": "9c34949c-821e-4f2d-b2c7-0fd8e8274cc1", - "created_date": "2024-08-16 23:58:36.213000", - "last_modified_date": "2024-08-16 23:58:36.213000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "9", - "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", - "volume_id": null - }, - { - "id": "9cb42533-c7d2-415b-9fdb-426db8f44233", - "created_date": "2024-08-16 23:58:36.596000", - "last_modified_date": "2024-08-16 23:58:36.596000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "18", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "9cdd4cce-8c13-4210-951a-c2cae94f0fb5", - "created_date": "2024-08-16 23:58:36.401000", - "last_modified_date": "2024-08-16 23:58:36.401000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "18", - "comic_id": "ed58b16e-0701-4373-befe-39118bc2d4cb", - "volume_id": null - }, - { - "id": "9d059360-cab1-48bc-b465-bb20ed0d5aa3", - "created_date": "2024-08-16 23:58:36.903000", - "last_modified_date": "2024-08-16 23:58:36.903000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "9aac6484-4c06-4286-815a-219aad25cc74", - "volume_id": null - }, - { - "id": "9d409127-97ed-4a4a-a88c-ef093b06fc79", - "created_date": "2024-08-16 23:58:36.378000", - "last_modified_date": "2024-08-16 23:58:36.378000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "af866a6a-2b51-499a-aa2d-b46743aabafd", - "volume_id": null - }, - { - "id": "9d5ef04a-4da7-49aa-a049-037e6b7430f7", - "created_date": "2024-08-16 23:58:35.958000", - "last_modified_date": "2024-08-16 23:58:35.958000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "14", - "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", - "volume_id": null - }, - { - "id": "9e620fb4-5300-42a7-b6d4-51efa0fe96c5", - "created_date": "2024-08-16 23:58:36.786000", - "last_modified_date": "2024-08-16 23:58:36.786000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", - "volume_id": null - }, - { - "id": "9edb244c-a3ea-44aa-aa66-2200ea07ef13", - "created_date": "2024-08-16 23:58:36.734000", - "last_modified_date": "2024-08-16 23:58:36.734000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "fe69cf80-946a-45d6-9fba-55ebfc0f038c", - "volume_id": null - }, - { - "id": "9f1c1f28-8d89-4efc-8004-8a8abb7af325", - "created_date": "2024-08-16 23:58:36.366000", - "last_modified_date": "2024-08-16 23:58:36.366000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "c91ec109-0b4d-4fd6-995f-1a828958493f", - "volume_id": null - }, - { - "id": "9fb6c042-10a2-4188-91ef-d83a02e8d364", - "created_date": "2024-08-16 23:58:35.973000", - "last_modified_date": "2024-08-16 23:58:35.973000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", - "volume_id": null - }, - { - "id": "a00e0a5f-aa6a-4838-8a2f-8c2101d45160", - "created_date": "2024-08-16 23:58:36.646000", - "last_modified_date": "2024-08-16 23:58:36.646000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "106", - "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", - "volume_id": null - }, - { - "id": "a12a705a-2efe-41f4-b3de-f48136cdb133", - "created_date": "2024-08-16 23:58:36.407000", - "last_modified_date": "2024-08-16 23:58:36.407000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "22", - "comic_id": "ed58b16e-0701-4373-befe-39118bc2d4cb", - "volume_id": null - }, - { - "id": "a1391e84-47b9-46c1-ac1b-5adebee90fcf", - "created_date": "2024-08-16 23:58:37.088000", - "last_modified_date": "2024-08-16 23:58:37.088000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "b4c866cb-9461-4aa4-bc3b-ef3e63848775", - "volume_id": null - }, - { - "id": "a1e17398-ca62-47d7-825c-8c7284c192ae", - "created_date": "2024-08-16 23:58:35.929000", - "last_modified_date": "2024-08-16 23:58:35.929000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", - "volume_id": null - }, - { - "id": "a24d1ab0-f050-4b77-a68e-c984146bbd2c", - "created_date": "2024-08-16 23:58:36.207000", - "last_modified_date": "2024-08-16 23:58:36.207000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", - "volume_id": null - }, - { - "id": "a274f0a4-e958-4146-bc08-a9b48e8e6b1c", - "created_date": "2024-08-16 23:58:36.141000", - "last_modified_date": "2024-08-16 23:58:36.141000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "c0543c9f-d712-4dce-9b59-2bf73b800b31", - "volume_id": null - }, - { - "id": "a277d0f5-6c96-4c05-96ec-c3d807a5ded0", - "created_date": "2024-08-16 23:58:35.861000", - "last_modified_date": "2024-08-16 23:58:35.861000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "14", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "a301150f-bf42-43f2-97b1-0142d16ad072", - "created_date": "2024-08-16 23:58:36.300000", - "last_modified_date": "2024-08-16 23:58:36.300000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "13281ef7-5945-49a9-b8f1-c5e8548c18ec", - "volume_id": null - }, - { - "id": "a31d9488-e89a-46bd-b5dd-37c90f38b416", - "created_date": "2024-08-16 23:58:36.631000", - "last_modified_date": "2024-08-16 23:58:36.631000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "0e6688d2-f347-4800-83b1-1f094b658084", - "volume_id": null - }, - { - "id": "a345f5e8-e740-4da3-b371-bd35cb4bc16e", - "created_date": "2024-08-16 23:58:36.792000", - "last_modified_date": "2024-08-16 23:58:36.792000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", - "volume_id": null - }, - { - "id": "a3b6f49c-081a-46c0-8a3d-cea66bb216b9", - "created_date": "2024-08-16 23:58:36.487000", - "last_modified_date": "2024-08-16 23:58:36.487000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "a499d5fc-543e-49e5-89f9-b39f1f170a5d", - "created_date": "2024-08-16 23:58:35.771000", - "last_modified_date": "2024-08-16 23:58:35.771000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "24", - "comic_id": "031b7570-04bc-4f56-834e-61c5792b6e5e", - "volume_id": null - }, - { - "id": "a49edcea-c031-452b-8877-283b85d8f95d", - "created_date": "2024-08-16 23:58:36.621000", - "last_modified_date": "2024-08-16 23:58:36.621000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "34", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "a5379fc8-0a35-4caa-978a-7d36b79cc688", - "created_date": "2024-08-16 23:58:35.686000", - "last_modified_date": "2024-08-16 23:58:35.686000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "a5e080b6-cd83-42b2-aef6-502d8d83dc9e", - "created_date": "2024-08-16 23:58:35.720000", - "last_modified_date": "2024-08-16 23:58:35.720000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "16", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "a6445817-0a9d-4696-8101-f37403e22d29", - "created_date": "2024-08-16 23:58:36.104000", - "last_modified_date": "2024-08-16 23:58:36.104000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "9b924cdc-8959-41e0-a84d-f3e61bbeac44", - "volume_id": null - }, - { - "id": "a67b1f04-0cf0-41d8-8b17-5a349d808447", - "created_date": "2024-08-16 23:58:36.790000", - "last_modified_date": "2024-08-16 23:58:36.790000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", - "volume_id": null - }, - { - "id": "a68c6312-c1e9-484f-a5ba-6733311f06af", - "created_date": "2024-08-16 23:58:35.813000", - "last_modified_date": "2024-08-16 23:58:35.813000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "89c5ea13-997a-4831-87cc-ee76ea05c71e", - "volume_id": null - }, - { - "id": "a72de1b3-f5e1-4ef8-ab2c-f07cfe80c208", - "created_date": "2024-08-16 23:58:36.782000", - "last_modified_date": "2024-08-16 23:58:36.782000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", - "volume_id": null - }, - { - "id": "a750bcf0-d8c2-41aa-a7c1-2db675f8b2b2", - "created_date": "2024-08-16 23:58:36.666000", - "last_modified_date": "2024-08-16 23:58:36.666000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "c9d4b26a-8431-4f68-8e7a-b61f2cf24176", - "volume_id": null - }, - { - "id": "a7a5a3cd-fe34-49f3-81ea-0c87f569afa2", - "created_date": "2024-08-16 23:58:35.999000", - "last_modified_date": "2024-08-16 23:58:35.999000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", - "volume_id": null - }, - { - "id": "a80b9018-dc6c-4e46-b696-7074ab32e6fe", - "created_date": "2024-08-16 23:58:36.626000", - "last_modified_date": "2024-08-16 23:58:36.626000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "0e6688d2-f347-4800-83b1-1f094b658084", - "volume_id": null - }, - { - "id": "a8ee84aa-6ac2-4ec2-b43a-4eaf93bf8a9f", - "created_date": "2024-08-16 23:58:36.764000", - "last_modified_date": "2024-08-16 23:58:36.764000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", - "volume_id": null - }, - { - "id": "a92c4a61-21c9-4b29-8171-25f3619d583a", - "created_date": "2024-08-16 23:58:36.615000", - "last_modified_date": "2024-08-16 23:58:36.615000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "30", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "a933279e-9c81-4f1d-8035-6fe54d593481", - "created_date": "2024-08-16 23:58:36.240000", - "last_modified_date": "2024-08-16 23:58:36.240000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "ff81bcf6-b368-4264-872c-544e85ec80e8", - "volume_id": null - }, - { - "id": "a94d8996-1af3-4820-8615-9ae5cafbace4", - "created_date": "2024-08-16 23:58:36.314000", - "last_modified_date": "2024-08-16 23:58:36.314000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "60deca87-7b2a-4412-855f-01a6ccaeea56", - "volume_id": null - }, - { - "id": "a9a4dee8-3481-4560-8122-480cd0a9048f", - "created_date": "2024-08-16 23:58:37.124000", - "last_modified_date": "2024-08-16 23:58:37.124000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", - "volume_id": null - }, - { - "id": "a9e41efc-1851-47fb-8ac8-58ae3e55ce93", - "created_date": "2024-08-16 23:58:37.122000", - "last_modified_date": "2024-08-16 23:58:37.122000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", - "volume_id": null - }, - { - "id": "a9f6688f-c7d4-4ee4-8dbd-a7d824a7a0c9", - "created_date": "2024-08-16 23:58:35.905000", - "last_modified_date": "2024-08-16 23:58:35.905000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "b73e5e2f-60e4-42fe-94bf-44fe3755b8b8", - "volume_id": null - }, - { - "id": "aa4c1fcf-d287-4fa4-ad94-b242048a82d8", - "created_date": "2024-08-16 23:58:37.132000", - "last_modified_date": "2024-08-16 23:58:37.132000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "10", - "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", - "volume_id": null - }, - { - "id": "ab08fb7b-ca64-45a2-9f06-dffff806e486", - "created_date": "2024-08-16 23:58:36.581000", - "last_modified_date": "2024-08-16 23:58:36.581000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "8955b2a3-84d3-4b55-a1f1-d45193600861", - "volume_id": null - }, - { - "id": "ab330c71-cc81-4e26-afe8-bee99e827868", - "created_date": "2024-08-16 23:58:36.080000", - "last_modified_date": "2024-08-16 23:58:36.080000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "11bc20d5-bfeb-4825-9e0f-3d6954020b07", - "volume_id": null - }, - { - "id": "ab5fdaf3-bb34-4b79-9f1c-c2d4671ffd3f", - "created_date": "2024-08-16 23:58:37.007000", - "last_modified_date": "2024-08-16 23:58:37.007000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "504", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "abbb4cd1-0bb1-4400-954e-1883f34af959", - "created_date": "2024-08-16 23:58:36.585000", - "last_modified_date": "2024-08-16 23:58:36.585000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "8955b2a3-84d3-4b55-a1f1-d45193600861", - "volume_id": null - }, - { - "id": "ac23dade-2c8b-45f0-82b7-e5bc091cd23a", - "created_date": "2024-08-16 23:58:36.784000", - "last_modified_date": "2024-08-16 23:58:36.784000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", - "volume_id": null - }, - { - "id": "ac4538d9-608c-4e34-9429-54e22fd34a2b", - "created_date": "2024-08-16 23:58:35.863000", - "last_modified_date": "2024-08-16 23:58:35.863000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "15", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "acac22ea-ceb6-4e24-a282-fa9ee6f7da34", - "created_date": "2024-08-16 23:58:36.284000", - "last_modified_date": "2024-08-16 23:58:36.284000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "3e96be30-f58f-459d-bb97-cfa574b9487c", - "volume_id": null - }, - { - "id": "ad4fece4-77bc-4881-8dbf-99111dc83ca8", - "created_date": "2024-08-16 23:58:36.745000", - "last_modified_date": "2024-08-16 23:58:36.745000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "fffaa1a6-5c3d-4deb-b759-35de74c65958", - "volume_id": null - }, - { - "id": "ad75d30b-3d98-4391-bfca-6e4cdbef806c", - "created_date": "2024-08-16 23:58:36.711000", - "last_modified_date": "2024-08-16 23:58:36.711000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "e1ff1410-ac3a-4ba5-8503-1fab529e50c0", - "volume_id": null - }, - { - "id": "ad7a91f0-696c-4e0e-bbe7-e65338ccd209", - "created_date": "2024-08-16 23:58:36.910000", - "last_modified_date": "2024-08-16 23:58:36.910000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "a08aef89-634c-494c-9def-73ff7e416464", - "volume_id": null - }, - { - "id": "ae1bc120-ffc2-4185-bf3f-80ee7aec2592", - "created_date": "2024-08-16 23:58:36.775000", - "last_modified_date": "2024-08-16 23:58:36.775000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "12", - "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", - "volume_id": null - }, - { - "id": "ae58fdba-00e6-4ec1-ba8e-e6cc52bc3f9b", - "created_date": "2024-08-16 23:58:35.623000", - "last_modified_date": "2024-08-16 23:58:35.623000", - "version": 0, - "in_stock": 0, - "is_read": 1, - "issue_number": "5", - "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", - "volume_id": null - }, - { - "id": "af196c8b-b378-4744-89cd-2ed48d09dc1c", - "created_date": "2024-08-16 23:58:37.010000", - "last_modified_date": "2024-08-16 23:58:37.010000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "506", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "af4ee406-36dc-4bfe-8012-58347773089e", - "created_date": "2024-08-16 23:58:36.007000", - "last_modified_date": "2024-08-16 23:58:36.007000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", - "volume_id": null - }, - { - "id": "af50b4f3-4347-448d-afb0-e3f4d933397a", - "created_date": "2024-08-16 23:58:36.191000", - "last_modified_date": "2024-08-16 23:58:36.191000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "fdccf7d0-db2c-4b01-a412-66e3ff043fe9", - "volume_id": null - }, - { - "id": "af63a085-daf4-4fdc-8e45-98b5ce65b889", - "created_date": "2024-08-16 23:58:36.592000", - "last_modified_date": "2024-08-16 23:58:36.592000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "17", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "af8ee98d-a3d1-4c68-a6c7-5ca433c5ee81", - "created_date": "2024-08-16 23:58:36.936000", - "last_modified_date": "2024-08-16 23:58:36.936000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "39", - "comic_id": "2a4b287e-4b05-4016-8eb4-21fc225be24d", - "volume_id": null - }, - { - "id": "b0a7be80-d8c1-4a3d-8cdc-c03b16def307", - "created_date": "2024-08-16 23:58:36.957000", - "last_modified_date": "2024-08-16 23:58:36.957000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "b0d12a8b-92d1-4550-bc8b-75ae6627db12", - "created_date": "2024-08-16 23:58:37.015000", - "last_modified_date": "2024-08-16 23:58:37.015000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "509", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "b23d0b43-5722-4b98-8ae8-81e09559f72f", - "created_date": "2024-08-16 23:58:36.477000", - "last_modified_date": "2024-08-16 23:58:36.477000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "33", - "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", - "volume_id": null - }, - { - "id": "b2806d77-812f-405b-a836-12cc2cb5ec9a", - "created_date": "2024-08-16 23:58:35.705000", - "last_modified_date": "2024-08-16 23:58:35.705000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "10", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "b2b3bda2-b141-46d6-b5d4-4bfa0062bd58", - "created_date": "2024-08-16 23:58:36.878000", - "last_modified_date": "2024-08-16 23:58:36.878000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", - "volume_id": null - }, - { - "id": "b390ad58-37d0-4cda-97fe-976976e937d7", - "created_date": "2024-08-16 23:58:35.805000", - "last_modified_date": "2024-08-16 23:58:35.805000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "b51c738d-8ba1-4451-8f89-f49c1968ac42", - "volume_id": null - }, - { - "id": "b3d78db2-794e-4498-8b68-3575d146736b", - "created_date": "2024-08-16 23:58:37.054000", - "last_modified_date": "2024-08-16 23:58:37.054000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "ce879e17-f391-4de0-81c5-3279cbc87fc8", - "volume_id": null - }, - { - "id": "b40e2560-309c-4184-afdf-64cfb124c57a", - "created_date": "2024-08-16 23:58:35.717000", - "last_modified_date": "2024-08-16 23:58:35.717000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "15", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "b481b432-dc5a-404c-9b7c-44fab39c349b", - "created_date": "2024-08-16 23:58:35.759000", - "last_modified_date": "2024-08-16 23:58:35.759000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "20", - "comic_id": "031b7570-04bc-4f56-834e-61c5792b6e5e", - "volume_id": null - }, - { - "id": "b4f7d577-cd9f-4be9-9383-452e1e832d5d", - "created_date": "2024-08-16 23:58:37.040000", - "last_modified_date": "2024-08-16 23:58:37.040000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "524", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "b5315225-d5f2-4f16-902b-051770bff973", - "created_date": "2024-08-16 23:58:36.262000", - "last_modified_date": "2024-08-16 23:58:36.262000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "5d0fd720-7875-4f9f-86eb-07ee0723908f", - "volume_id": null - }, - { - "id": "b575864b-dd9e-4544-8822-f56c49f6c5ff", - "created_date": "2024-08-16 23:58:36.864000", - "last_modified_date": "2024-08-16 23:58:36.864000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "11", - "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", - "volume_id": null - }, - { - "id": "b58c0981-7d0f-4158-aaab-b04b25290151", - "created_date": "2024-08-16 23:58:36.031000", - "last_modified_date": "2024-08-16 23:58:36.031000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "bf23d317-f5b6-4cf8-8a05-4397888a82c9", - "volume_id": null - }, - { - "id": "b5a308f9-f82f-4b28-bbfd-c9168ac292ec", - "created_date": "2024-08-16 23:58:36.055000", - "last_modified_date": "2024-08-16 23:58:36.055000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", - "volume_id": null - }, - { - "id": "b5c0baff-80a1-43e5-bd16-cc1bd1145171", - "created_date": "2024-08-16 23:58:36.015000", - "last_modified_date": "2024-08-16 23:58:36.015000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "11", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", - "volume_id": null - }, - { - "id": "b5e3881b-b949-4f2b-b899-a9db5d4dd48f", - "created_date": "2024-08-16 23:58:36.951000", - "last_modified_date": "2024-08-16 23:58:36.951000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "35894d94-1fb5-49de-ba77-cf2626cda939", - "volume_id": null - }, - { - "id": "b61866b2-7bf9-4b3c-bce7-7be170b23bde", - "created_date": "2024-08-16 23:58:36.150000", - "last_modified_date": "2024-08-16 23:58:36.150000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "5fc6600f-b005-4d6b-a1af-a17cc2701d81", - "volume_id": null - }, - { - "id": "b67f8cae-5eea-4bfb-a945-14cc916b4173", - "created_date": "2024-08-16 23:58:35.915000", - "last_modified_date": "2024-08-16 23:58:35.915000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", - "volume_id": null - }, - { - "id": "b6b97dc6-d72b-496a-b6fd-f2757e55bddb", - "created_date": "2024-08-16 23:58:37.105000", - "last_modified_date": "2024-08-16 23:58:37.105000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "639f7e71-1012-49a6-bc3a-1ac7b1de3084", - "volume_id": null - }, - { - "id": "b6ffc679-b36a-4f5d-9e32-2d7790a3d97a", - "created_date": "2024-08-16 23:58:36.753000", - "last_modified_date": "2024-08-16 23:58:36.753000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "4f0e16a3-452f-43cd-9834-d0f4d2d5fca0", - "volume_id": null - }, - { - "id": "b700e1ac-04f4-4523-837d-92ee60b9e76f", - "created_date": "2024-08-16 23:58:35.599000", - "last_modified_date": "2024-08-16 23:58:35.599000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "5dbe4d8b-331a-41ad-bcdc-01196dc1d58d", - "volume_id": null - }, - { - "id": "b743d1ea-8c78-42e1-bba2-099031cc4acb", - "created_date": "2024-08-16 23:58:36.447000", - "last_modified_date": "2024-08-16 23:58:36.447000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "21", - "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", - "volume_id": null - }, - { - "id": "b798b880-359e-4f49-ba1e-97c7cc89739b", - "created_date": "2024-08-16 23:58:35.739000", - "last_modified_date": "2024-08-16 23:58:35.739000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "22", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "b8746fb8-fcc5-4c79-bc34-37a5dd17ce25", - "created_date": "2024-08-16 23:58:36.525000", - "last_modified_date": "2024-08-16 23:58:36.525000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "25", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "b8a7c97a-2117-4be7-a578-094dd55d7104", - "created_date": "2024-08-16 23:58:36.573000", - "last_modified_date": "2024-08-16 23:58:36.573000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "0095a769-e5e2-441e-8a38-f98743ee9529", - "volume_id": null - }, - { - "id": "b8b60c2c-21d7-44af-953f-744061e5ca93", - "created_date": "2024-08-16 23:58:36.740000", - "last_modified_date": "2024-08-16 23:58:36.740000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "a557dbbc-2af1-4b56-8588-8fe42cf5c454", - "volume_id": null - }, - { - "id": "b8cf8ca0-3371-4afc-8b73-b64b677f521c", - "created_date": "2024-08-16 23:58:37.073000", - "last_modified_date": "2024-08-16 23:58:37.073000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "231e65eb-eecb-4946-a643-6184b767e321", - "volume_id": null - }, - { - "id": "b91a18e1-c6b7-47c0-8136-e64e48ea90e8", - "created_date": "2024-08-16 23:58:36.799000", - "last_modified_date": "2024-08-16 23:58:36.799000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "12", - "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", - "volume_id": null - }, - { - "id": "b9288042-d911-49de-aba2-623ebede9448", - "created_date": "2024-08-16 23:58:36.883000", - "last_modified_date": "2024-08-16 23:58:36.883000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "8", - "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", - "volume_id": null - }, - { - "id": "b940d779-7d53-4b14-a408-7c80ab149f53", - "created_date": "2024-08-16 23:58:36.630000", - "last_modified_date": "2024-08-16 23:58:36.630000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "0e6688d2-f347-4800-83b1-1f094b658084", - "volume_id": null - }, - { - "id": "b9bff414-cf25-4903-87aa-33dd18e6e292", - "created_date": "2024-08-16 23:58:37.084000", - "last_modified_date": "2024-08-16 23:58:37.084000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "b4c866cb-9461-4aa4-bc3b-ef3e63848775", - "volume_id": null - }, - { - "id": "b9e5781b-0db0-4802-bd2b-8e415419e281", - "created_date": "2024-08-16 23:58:36.308000", - "last_modified_date": "2024-08-16 23:58:36.308000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "60deca87-7b2a-4412-855f-01a6ccaeea56", - "volume_id": null - }, - { - "id": "ba447c99-89c1-437c-b51d-a8ae1a136786", - "created_date": "2024-08-16 23:58:36.988000", - "last_modified_date": "2024-08-16 23:58:36.988000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "19", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "ba5d88bb-5dff-4868-9c94-9d5d7f16873b", - "created_date": "2024-08-16 23:58:36.512000", - "last_modified_date": "2024-08-16 23:58:36.512000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "19", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "ba8b0cd5-ba0c-49e3-9e85-61fb968c9f4d", - "created_date": "2024-08-16 23:58:35.898000", - "last_modified_date": "2024-08-16 23:58:35.898000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "b73e5e2f-60e4-42fe-94bf-44fe3755b8b8", - "volume_id": null - }, - { - "id": "bb052694-2093-49dc-8351-151e96edd36d", - "created_date": "2024-08-16 23:58:36.489000", - "last_modified_date": "2024-08-16 23:58:36.489000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "bb0f953b-bdec-49a6-9113-a52f9d23e1bf", - "created_date": "2024-08-16 23:58:35.976000", - "last_modified_date": "2024-08-16 23:58:35.976000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", - "volume_id": null - }, - { - "id": "bb7013b3-7113-4b4f-b29f-0ab50ea0c263", - "created_date": "2024-08-16 23:58:36.399000", - "last_modified_date": "2024-08-16 23:58:36.399000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "17", - "comic_id": "ed58b16e-0701-4373-befe-39118bc2d4cb", - "volume_id": null - }, - { - "id": "bb92f351-a98a-4694-9b87-a065c4eecf5b", - "created_date": "2024-08-16 23:58:35.980000", - "last_modified_date": "2024-08-16 23:58:35.980000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "9", - "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", - "volume_id": null - }, - { - "id": "bbb9ed06-b275-49f7-8894-2227d00b312a", - "created_date": "2024-08-16 23:58:36.817000", - "last_modified_date": "2024-08-16 23:58:36.817000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "12802b17-452a-4533-a7ab-fd94f19be123", - "volume_id": null - }, - { - "id": "bc5939d5-b7a9-473a-8beb-94e149808f6a", - "created_date": "2024-08-16 23:58:36.853000", - "last_modified_date": "2024-08-16 23:58:36.853000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", - "volume_id": null - }, - { - "id": "bc8a7b49-32cb-4280-85c2-41bce35e6c20", - "created_date": "2024-08-16 23:58:36.618000", - "last_modified_date": "2024-08-16 23:58:36.618000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "32", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "bca16695-676a-4a8a-8727-b295285bb491", - "created_date": "2024-08-16 23:58:36.510000", - "last_modified_date": "2024-08-16 23:58:36.510000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "18", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "bcb95bea-43cb-4e3a-a206-5a7278b48805", - "created_date": "2024-08-16 23:58:37.050000", - "last_modified_date": "2024-08-16 23:58:37.050000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "b9a8abf9-b259-4a6f-be2d-75f211788440", - "volume_id": null - }, - { - "id": "bd50d299-efe0-43e7-98ef-d05b7cc828ad", - "created_date": "2024-08-16 23:58:36.420000", - "last_modified_date": "2024-08-16 23:58:36.420000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "3217d912-3b46-47d2-a6f8-a79c1eecfde1", - "volume_id": null - }, - { - "id": "bdd8560a-42cc-418d-a582-fabba88bb405", - "created_date": "2024-08-16 23:58:36.348000", - "last_modified_date": "2024-08-16 23:58:36.348000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "10", - "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", - "volume_id": null - }, - { - "id": "be092458-4540-4825-939d-bb45df840b06", - "created_date": "2024-08-16 23:58:36.369000", - "last_modified_date": "2024-08-16 23:58:36.369000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "b6713944-8d2d-4153-8f16-fe94cc4ee119", - "volume_id": null - }, - { - "id": "be1f11e7-3d76-4fca-98c2-4a903832c7d3", - "created_date": "2024-08-16 23:58:36.068000", - "last_modified_date": "2024-08-16 23:58:36.068000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "10", - "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", - "volume_id": null - }, - { - "id": "be21d1d9-9ee8-4c59-b245-0c5a0242d3bc", - "created_date": "2024-08-16 23:58:36.702000", - "last_modified_date": "2024-08-16 23:58:36.702000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "154", - "comic_id": "cc96b25e-b827-4ff0-a94a-b82d30ca883c", - "volume_id": null - }, - { - "id": "be368ff7-bbd0-472d-bac4-56683e9d4ae2", - "created_date": "2024-08-16 23:58:36.824000", - "last_modified_date": "2024-08-16 23:58:36.824000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "0", - "comic_id": "8e293af3-05c6-4dcc-9cbf-b87512ec975b", - "volume_id": null - }, - { - "id": "be7dc008-f661-4d60-871f-06d38cb40718", - "created_date": "2024-08-16 23:58:36.821000", - "last_modified_date": "2024-08-16 23:58:36.821000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "dc81f255-6757-4c41-8a7b-a06a503d7daa", - "volume_id": null - }, - { - "id": "bf1e55ce-7152-4cab-8f0f-3e728060d6f4", - "created_date": "2024-08-16 23:58:35.613000", - "last_modified_date": "2024-08-16 23:58:35.613000", - "version": 0, - "in_stock": 0, - "is_read": 1, - "issue_number": "2", - "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", - "volume_id": null - }, - { - "id": "bf7bad86-d121-42da-b4a1-fe139b1bf47f", - "created_date": "2024-08-16 23:58:35.816000", - "last_modified_date": "2024-08-16 23:58:35.816000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "89c5ea13-997a-4831-87cc-ee76ea05c71e", - "volume_id": null - }, - { - "id": "bf7ed47b-4c54-4b8d-8add-a85c4eee7d3a", - "created_date": "2024-08-16 23:58:35.757000", - "last_modified_date": "2024-08-16 23:58:35.757000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "11", - "comic_id": "d7dd6d02-bc9a-4fbf-a5ca-ad4728cb8109", - "volume_id": null - }, - { - "id": "c03383be-6867-4e9c-af5d-c9354e9ec268", - "created_date": "2024-08-16 23:58:36.129000", - "last_modified_date": "2024-08-16 23:58:36.129000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "f3231681-cd2b-4ff9-bfa2-5d8f631bee4d", - "volume_id": null - }, - { - "id": "c121830e-9454-42f9-a7d4-909fea57ff60", - "created_date": "2024-08-16 23:58:37.002000", - "last_modified_date": "2024-08-16 23:58:37.002000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "851bc748-2602-4f20-826f-a59a7087d11f", - "volume_id": null - }, - { - "id": "c1446de6-b76f-49d4-a685-c9369415b166", - "created_date": "2024-08-16 23:58:35.966000", - "last_modified_date": "2024-08-16 23:58:35.966000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", - "volume_id": null - }, - { - "id": "c1dc88c9-a523-4db6-b8cd-e51ad0cd8216", - "created_date": "2024-08-16 23:58:37.128000", - "last_modified_date": "2024-08-16 23:58:37.128000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "8", - "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", - "volume_id": null - }, - { - "id": "c1de66d3-9e35-4ecd-8e48-c66465149dcf", - "created_date": "2024-08-16 23:58:36.972000", - "last_modified_date": "2024-08-16 23:58:36.972000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "9", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "c254525b-8240-4580-a555-97089523f61c", - "created_date": "2024-08-16 23:58:36.342000", - "last_modified_date": "2024-08-16 23:58:36.342000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", - "volume_id": null - }, - { - "id": "c2713db3-c3af-47e9-aa46-7065842c1fc3", - "created_date": "2024-08-16 23:58:36.375000", - "last_modified_date": "2024-08-16 23:58:36.375000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "e048cec2-52b9-48f8-9975-cfe17ed85aae", - "volume_id": null - }, - { - "id": "c3089517-03e0-4b76-97c1-2931a84d180b", - "created_date": "2024-08-16 23:58:36.561000", - "last_modified_date": "2024-08-16 23:58:36.561000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "100b82bf-c134-40d9-bcc2-a8b345030b8d", - "volume_id": null - }, - { - "id": "c352f3c2-a152-41d7-894b-06e36900af02", - "created_date": "2024-08-16 23:58:36.577000", - "last_modified_date": "2024-08-16 23:58:36.577000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "0095a769-e5e2-441e-8a38-f98743ee9529", - "volume_id": null - }, - { - "id": "c356c1d8-896d-4da1-9dfa-6219a637e99d", - "created_date": "2024-08-16 23:58:35.751000", - "last_modified_date": "2024-08-16 23:58:35.751000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", - "volume_id": null - }, - { - "id": "c3613366-80bd-443b-b86b-d939750af351", - "created_date": "2024-08-16 23:58:35.667000", - "last_modified_date": "2024-08-16 23:58:35.667000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", - "volume_id": null - }, - { - "id": "c37b1411-1c5a-46f2-b35d-4b8e92496a2c", - "created_date": "2024-08-16 23:58:35.997000", - "last_modified_date": "2024-08-16 23:58:35.997000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", - "volume_id": null - }, - { - "id": "c3913ad6-ef63-4d27-b588-5bcf16ee5807", - "created_date": "2024-08-16 23:58:36.692000", - "last_modified_date": "2024-08-16 23:58:36.692000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "14", - "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", - "volume_id": null - }, - { - "id": "c3efa06e-6516-4f9f-b07e-bf3c0e140852", - "created_date": "2024-08-16 23:58:36.498000", - "last_modified_date": "2024-08-16 23:58:36.498000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "11", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "c4064467-0a2a-4422-bcaf-d25b081d9fe0", - "created_date": "2024-08-16 23:58:36.085000", - "last_modified_date": "2024-08-16 23:58:36.085000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "11bc20d5-bfeb-4825-9e0f-3d6954020b07", - "volume_id": null - }, - { - "id": "c41ea90c-a8ce-430c-8851-ec9b24e5ae82", - "created_date": "2024-08-16 23:58:36.328000", - "last_modified_date": "2024-08-16 23:58:36.328000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "7c110b15-bbfc-472e-830b-b8db6ddb274e", - "volume_id": null - }, - { - "id": "c531bbba-d203-4c4a-bc00-06a202cac746", - "created_date": "2024-08-16 23:58:37.086000", - "last_modified_date": "2024-08-16 23:58:37.086000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "b4c866cb-9461-4aa4-bc3b-ef3e63848775", - "volume_id": null - }, - { - "id": "c5a771bb-4d01-4174-acb7-4b05c66965e1", - "created_date": "2024-08-16 23:58:36.990000", - "last_modified_date": "2024-08-16 23:58:36.990000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "20", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "c5b0f36b-c457-47bf-914f-7826bdad22b5", - "created_date": "2024-08-16 23:58:37.008000", - "last_modified_date": "2024-08-16 23:58:37.008000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "505", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "c5b5a81f-dff7-4d55-a215-b532f2ec90f5", - "created_date": "2024-08-16 23:58:36.896000", - "last_modified_date": "2024-08-16 23:58:36.896000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "0fd9969e-8e1f-4f42-94ac-fab4d2d614c2", - "volume_id": null - }, - { - "id": "c5c73a82-b5a5-43fb-827b-b8e8eea8f49b", - "created_date": "2024-08-16 23:58:35.695000", - "last_modified_date": "2024-08-16 23:58:35.695000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "c69cc23d-5645-436f-a800-d577cbd64662", - "created_date": "2024-08-16 23:58:36.445000", - "last_modified_date": "2024-08-16 23:58:36.445000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "20", - "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", - "volume_id": null - }, - { - "id": "c9641ed7-7ab3-4c0f-8f1b-daecccfc4088", - "created_date": "2024-08-16 23:58:36.704000", - "last_modified_date": "2024-08-16 23:58:36.704000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "a2409ef1-82c3-45d1-9c65-b8806a31e525", - "volume_id": null - }, - { - "id": "c984ccce-35fe-467f-b30a-c18c9df93aa7", - "created_date": "2024-08-16 23:58:37.044000", - "last_modified_date": "2024-08-16 23:58:37.044000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "526", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "c9d86579-7243-4bc7-8381-9a33a6770e9b", - "created_date": "2024-08-16 23:58:36.323000", - "last_modified_date": "2024-08-16 23:58:36.323000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "7c110b15-bbfc-472e-830b-b8db6ddb274e", - "volume_id": null - }, - { - "id": "c9da40fe-00d5-4291-88c2-0987e5f45a3e", - "created_date": "2024-08-16 23:58:37.103000", - "last_modified_date": "2024-08-16 23:58:37.103000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "07574acf-8f77-4da7-87fd-c49f40536baf", - "volume_id": null - }, - { - "id": "ca026a61-30bd-452c-9943-610b20a263e9", - "created_date": "2024-08-16 23:58:36.517000", - "last_modified_date": "2024-08-16 23:58:36.517000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "22", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "ca4c8f3f-1e50-4d25-8090-46ba93661c35", - "created_date": "2024-08-16 23:58:36.325000", - "last_modified_date": "2024-08-16 23:58:36.325000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "7c110b15-bbfc-472e-830b-b8db6ddb274e", - "volume_id": null - }, - { - "id": "ca9ac5fe-0e69-46a0-a3e0-570e2267958a", - "created_date": "2024-08-16 23:58:36.332000", - "last_modified_date": "2024-08-16 23:58:36.332000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", - "volume_id": null - }, - { - "id": "cb347040-e464-44db-b9d4-9a3ae66cdef5", - "created_date": "2024-08-16 23:58:36.642000", - "last_modified_date": "2024-08-16 23:58:36.642000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "103", - "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", - "volume_id": null - }, - { - "id": "cb68308e-5c1f-4476-ab73-c95066047ec8", - "created_date": "2024-08-16 23:58:36.075000", - "last_modified_date": "2024-08-16 23:58:36.075000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "13", - "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", - "volume_id": null - }, - { - "id": "cb88d324-ef6e-42e1-8c03-a6e0b6b78f44", - "created_date": "2024-08-16 23:58:36.506000", - "last_modified_date": "2024-08-16 23:58:36.506000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "16", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "cbff57c0-e21f-4a69-ae77-45aa7f13e63e", - "created_date": "2024-08-16 23:58:37.127000", - "last_modified_date": "2024-08-16 23:58:37.127000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", - "volume_id": null - }, - { - "id": "cc01f0d9-377f-4393-b52c-312edacbeb56", - "created_date": "2024-08-16 23:58:36.346000", - "last_modified_date": "2024-08-16 23:58:36.346000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "9", - "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", - "volume_id": null - }, - { - "id": "cc86eb80-56c7-4865-8811-615a566fb8cb", - "created_date": "2024-08-16 23:58:36.169000", - "last_modified_date": "2024-08-16 23:58:36.169000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", - "volume_id": null - }, - { - "id": "ccda3fa2-248f-40a5-9d1f-2ddd9972772c", - "created_date": "2024-08-16 23:58:36.758000", - "last_modified_date": "2024-08-16 23:58:36.758000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", - "volume_id": null - }, - { - "id": "cd04f942-1be1-4d6f-9e0b-2c6144b9a7ca", - "created_date": "2024-08-16 23:58:36.564000", - "last_modified_date": "2024-08-16 23:58:36.564000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "45bd0c8a-845f-4ab3-9281-c31e5a3d4472", - "volume_id": null - }, - { - "id": "cdc011cf-2fab-4521-adee-06bbcaf0f583", - "created_date": "2024-08-16 23:58:37.133000", - "last_modified_date": "2024-08-16 23:58:37.133000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "11", - "comic_id": "8a4558ac-33e9-4656-ab47-8292af313ff7", - "volume_id": null - }, - { - "id": "cdf07ebc-d6e0-4667-8632-678c18b3b866", - "created_date": "2024-08-16 23:58:36.233000", - "last_modified_date": "2024-08-16 23:58:36.233000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "ff81bcf6-b368-4264-872c-544e85ec80e8", - "volume_id": null - }, - { - "id": "cdfc9a86-33e6-4554-9a60-a03023a0d58a", - "created_date": "2024-08-16 23:58:36.029000", - "last_modified_date": "2024-08-16 23:58:36.029000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "18", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", - "volume_id": null - }, - { - "id": "ce348d2c-d1e3-4d3c-9b14-4fe917891c10", - "created_date": "2024-08-16 23:58:36.503000", - "last_modified_date": "2024-08-16 23:58:36.503000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "14", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "ce4a5c02-2ce1-4264-8ff1-526544f14a9e", - "created_date": "2024-08-16 23:58:36.196000", - "last_modified_date": "2024-08-16 23:58:36.196000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "efc52177-93a0-4e69-a76f-2c9049ff3967", - "volume_id": null - }, - { - "id": "cefd3450-38a0-4b7c-b917-8931c03d85fc", - "created_date": "2024-08-16 23:58:36.584000", - "last_modified_date": "2024-08-16 23:58:36.584000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "8955b2a3-84d3-4b55-a1f1-d45193600861", - "volume_id": null - }, - { - "id": "cfcf2d8c-41bb-4783-9d04-45109eed329d", - "created_date": "2024-08-16 23:58:36.238000", - "last_modified_date": "2024-08-16 23:58:36.238000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "ff81bcf6-b368-4264-872c-544e85ec80e8", - "volume_id": null - }, - { - "id": "d134ea40-2790-4960-9c6b-c99ba4e45fbb", - "created_date": "2024-08-16 23:58:35.924000", - "last_modified_date": "2024-08-16 23:58:35.924000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", - "volume_id": null - }, - { - "id": "d139d42d-b36d-4d38-8f48-6f0ae7ba4bff", - "created_date": "2024-08-16 23:58:36.418000", - "last_modified_date": "2024-08-16 23:58:36.418000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "010ffe6b-54e4-42d7-86b6-581e680c35ad", - "volume_id": null - }, - { - "id": "d1a924cc-08b3-4465-a9b3-be10958aadb4", - "created_date": "2024-08-16 23:58:36.303000", - "last_modified_date": "2024-08-16 23:58:36.303000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "13281ef7-5945-49a9-b8f1-c5e8548c18ec", - "volume_id": null - }, - { - "id": "d1b48b90-abb2-445f-823d-e0c721355abc", - "created_date": "2024-08-16 23:58:36.645000", - "last_modified_date": "2024-08-16 23:58:36.645000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "105", - "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", - "volume_id": null - }, - { - "id": "d1e7182b-a0e7-4d6d-99ce-80ea1a0934ae", - "created_date": "2024-08-16 23:58:36.471000", - "last_modified_date": "2024-08-16 23:58:36.471000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "31", - "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", - "volume_id": null - }, - { - "id": "d22f3e1e-4fba-43b3-b4f1-d8d1e14657c4", - "created_date": "2024-08-16 23:58:36.100000", - "last_modified_date": "2024-08-16 23:58:36.100000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "9b924cdc-8959-41e0-a84d-f3e61bbeac44", - "volume_id": null - }, - { - "id": "d26715ca-db32-4f78-bb34-bae678419439", - "created_date": "2024-08-16 23:58:36.918000", - "last_modified_date": "2024-08-16 23:58:36.918000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "df47cdb1-0b41-4baa-83ce-fcfbb1c2bd51", - "volume_id": null - }, - { - "id": "d3425ed8-c9a4-478f-972e-d40d219a75e3", - "created_date": "2024-08-16 23:58:36.928000", - "last_modified_date": "2024-08-16 23:58:36.928000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "df47cdb1-0b41-4baa-83ce-fcfbb1c2bd51", - "volume_id": null - }, - { - "id": "d374a493-e80b-4bce-91f5-85abce983585", - "created_date": "2024-08-16 23:58:36.200000", - "last_modified_date": "2024-08-16 23:58:36.200000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", - "volume_id": null - }, - { - "id": "d3dcf1ce-52a1-41eb-bbfe-691531a197d9", - "created_date": "2024-08-16 23:58:36.062000", - "last_modified_date": "2024-08-16 23:58:36.062000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "8", - "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", - "volume_id": null - }, - { - "id": "d4655e35-d8e9-437f-836f-cbe52f5c2060", - "created_date": "2024-08-16 23:58:35.798000", - "last_modified_date": "2024-08-16 23:58:35.798000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "b51c738d-8ba1-4451-8f89-f49c1968ac42", - "volume_id": null - }, - { - "id": "d49ae93e-c0da-44b0-9938-5524cba4a1cc", - "created_date": "2024-08-16 23:58:37.019000", - "last_modified_date": "2024-08-16 23:58:37.019000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "512", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "d4af7d68-9f41-4926-a541-18c7d76594fa", - "created_date": "2024-08-16 23:58:36.350000", - "last_modified_date": "2024-08-16 23:58:36.350000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "11", - "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", - "volume_id": null - }, - { - "id": "d4f99702-dccc-42d7-bd28-058393cd83f8", - "created_date": "2024-08-16 23:58:37.076000", - "last_modified_date": "2024-08-16 23:58:37.076000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "231e65eb-eecb-4946-a643-6184b767e321", - "volume_id": null - }, - { - "id": "d53f7493-72e1-45d5-a573-c45ae912dca6", - "created_date": "2024-08-16 23:58:36.623000", - "last_modified_date": "2024-08-16 23:58:36.623000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "1d9440ba-202a-4c4e-8425-8561606c1c3e", - "volume_id": null - }, - { - "id": "d553ba7e-cb3b-4fb1-b1be-24fea1622edc", - "created_date": "2024-08-16 23:58:36.175000", - "last_modified_date": "2024-08-16 23:58:36.175000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", - "volume_id": null - }, - { - "id": "d569785a-015b-4e57-912d-a9e6ea94d5e8", - "created_date": "2024-08-16 23:58:35.987000", - "last_modified_date": "2024-08-16 23:58:35.987000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "12", - "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", - "volume_id": null - }, - { - "id": "d570c5aa-6631-4fd1-a727-4984f6a4f17f", - "created_date": "2024-08-16 23:58:36.974000", - "last_modified_date": "2024-08-16 23:58:36.974000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "10", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "d59bb10b-fe68-400c-b226-2efdc1f7a5dd", - "created_date": "2024-08-16 23:58:36.719000", - "last_modified_date": "2024-08-16 23:58:36.719000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "e1ff1410-ac3a-4ba5-8503-1fab529e50c0", - "volume_id": null - }, - { - "id": "d61c1d1a-8834-4e3e-a8d3-ecbbb6a6dd46", - "created_date": "2024-08-16 23:58:36.193000", - "last_modified_date": "2024-08-16 23:58:36.193000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "efc52177-93a0-4e69-a76f-2c9049ff3967", - "volume_id": null - }, - { - "id": "d6593b47-10e2-4bf7-8c65-efae8bdc1ce9", - "created_date": "2024-08-16 23:58:35.781000", - "last_modified_date": "2024-08-16 23:58:35.781000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "5a72a63d-8fbd-46a8-b201-9b6e035c782a", - "volume_id": null - }, - { - "id": "d676914f-d1cf-42f9-a552-49ce09f4bb37", - "created_date": "2024-08-16 23:58:36.024000", - "last_modified_date": "2024-08-16 23:58:36.024000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "15", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", - "volume_id": null - }, - { - "id": "d67961ad-d8bb-43ee-9a16-df132e255bb8", - "created_date": "2024-08-16 23:58:35.943000", - "last_modified_date": "2024-08-16 23:58:35.943000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "9", - "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", - "volume_id": null - }, - { - "id": "d6906f96-d8b3-428f-b006-c61a23cab7f0", - "created_date": "2024-08-16 23:58:37.047000", - "last_modified_date": "2024-08-16 23:58:37.047000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "a5987e5c-0245-484d-aeb8-4b5195800d66", - "volume_id": null - }, - { - "id": "d6908f9e-79e3-4e04-b3d0-33328fe13bc7", - "created_date": "2024-08-16 23:58:35.836000", - "last_modified_date": "2024-08-16 23:58:35.836000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "d6e1d653-5262-4f1a-9260-799e37bd7cd4", - "created_date": "2024-08-16 23:58:36.484000", - "last_modified_date": "2024-08-16 23:58:36.484000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "d7013d89-ac50-4094-8312-18dd3be11d07", - "created_date": "2024-08-16 23:58:35.634000", - "last_modified_date": "2024-08-16 23:58:35.634000", - "version": 0, - "in_stock": 0, - "is_read": 1, - "issue_number": "9", - "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", - "volume_id": null - }, - { - "id": "d70d8f9a-9501-4e07-af2b-519ebc2ca2aa", - "created_date": "2024-08-16 23:58:36.588000", - "last_modified_date": "2024-08-16 23:58:36.588000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "14", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "d78a1e50-9808-43c4-9c57-f167627ef699", - "created_date": "2024-08-16 23:58:36.725000", - "last_modified_date": "2024-08-16 23:58:36.725000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "25841b05-c246-4484-9ef2-71f8dcdb39ed", - "volume_id": null - }, - { - "id": "d850c042-b793-4a23-9391-1c5cfcc27011", - "created_date": "2024-08-16 23:58:37.011000", - "last_modified_date": "2024-08-16 23:58:37.011000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "507", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "d9cf1648-0326-4201-b687-64e3ed616118", - "created_date": "2024-08-16 23:58:36.891000", - "last_modified_date": "2024-08-16 23:58:36.891000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "80f818f1-3813-4bf9-9eb2-0a441799fa6d", - "volume_id": null - }, - { - "id": "db290641-7a7e-4349-a999-ade57a73842d", - "created_date": "2024-08-16 23:58:35.696000", - "last_modified_date": "2024-08-16 23:58:35.696000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "dbb71353-f0a6-4c13-b58f-d5b135d3956a", - "created_date": "2024-08-16 23:58:36.273000", - "last_modified_date": "2024-08-16 23:58:36.273000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "ef71fb0f-ecba-4d88-ae5f-90c81b0c4e18", - "volume_id": null - }, - { - "id": "dbbd1211-03ba-4770-b5af-fa99947509f7", - "created_date": "2024-08-16 23:58:36.248000", - "last_modified_date": "2024-08-16 23:58:36.248000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "639eed1d-3ccf-4bfa-a595-06b44a4e5b8f", - "volume_id": null - }, - { - "id": "dd067de8-d411-4abc-95ba-060821dc3bc8", - "created_date": "2024-08-16 23:58:36.154000", - "last_modified_date": "2024-08-16 23:58:36.154000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "5fc6600f-b005-4d6b-a1af-a17cc2701d81", - "volume_id": null - }, - { - "id": "dd11fe97-780e-42b9-a8a0-b7c30b71a0ea", - "created_date": "2024-08-16 23:58:36.613000", - "last_modified_date": "2024-08-16 23:58:36.613000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "29", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc", - "volume_id": null - }, - { - "id": "dd2843ff-5c4a-455e-a6a1-9836b641baee", - "created_date": "2024-08-16 23:58:36.751000", - "last_modified_date": "2024-08-16 23:58:36.751000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "4f0e16a3-452f-43cd-9834-d0f4d2d5fca0", - "volume_id": null - }, - { - "id": "dd617f85-37ef-4d53-967c-8806c78a6e01", - "created_date": "2024-08-16 23:58:36.961000", - "last_modified_date": "2024-08-16 23:58:36.961000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "dd97ad26-b348-4a03-b5bf-e635295fa24c", - "created_date": "2024-08-16 23:58:36.505000", - "last_modified_date": "2024-08-16 23:58:36.505000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "15", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "de0b91de-6de5-489b-a192-73c095184887", - "created_date": "2024-08-16 23:58:36.491000", - "last_modified_date": "2024-08-16 23:58:36.491000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "ded7fec4-528e-44b2-9a8d-bd3221f9c2fc", - "created_date": "2024-08-16 23:58:36.897000", - "last_modified_date": "2024-08-16 23:58:36.897000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "799309bc-d8d9-4d44-9457-bae7f1e7fcbf", - "volume_id": null - }, - { - "id": "dee5fa1b-7eae-4a3c-bdb8-0704b102ff38", - "created_date": "2024-08-16 23:58:36.171000", - "last_modified_date": "2024-08-16 23:58:36.171000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", - "volume_id": null - }, - { - "id": "df0e1478-a0b2-41c0-bee3-b23143eb6590", - "created_date": "2024-08-16 23:58:36.823000", - "last_modified_date": "2024-08-16 23:58:36.823000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "c0a044dd-461b-459a-9962-b94ed0e8b38f", - "volume_id": null - }, - { - "id": "df67806e-4968-4abe-80f8-802e1cb43b6e", - "created_date": "2024-08-16 23:58:35.637000", - "last_modified_date": "2024-08-16 23:58:35.637000", - "version": 0, - "in_stock": 0, - "is_read": 1, - "issue_number": "10", - "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", - "volume_id": null - }, - { - "id": "df6c8680-36d4-459a-9e96-7ce14c4bf03d", - "created_date": "2024-08-16 23:58:35.784000", - "last_modified_date": "2024-08-16 23:58:35.784000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "5a72a63d-8fbd-46a8-b201-9b6e035c782a", - "volume_id": null - }, - { - "id": "dfde9a1b-2903-472c-8242-35ff9278b08e", - "created_date": "2024-08-16 23:58:35.833000", - "last_modified_date": "2024-08-16 23:58:35.833000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "e0171122-e40f-471d-a6c8-42bfce7c78d9", - "created_date": "2024-08-16 23:58:36.944000", - "last_modified_date": "2024-08-16 23:58:36.944000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "44", - "comic_id": "2a4b287e-4b05-4016-8eb4-21fc225be24d", - "volume_id": null - }, - { - "id": "e095447f-eaf5-4608-b1ac-176587174252", - "created_date": "2024-08-16 23:58:37.107000", - "last_modified_date": "2024-08-16 23:58:37.107000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "639f7e71-1012-49a6-bc3a-1ac7b1de3084", - "volume_id": null - }, - { - "id": "e0db2046-963a-4889-bc27-dbd28c0e5c54", - "created_date": "2024-08-16 23:58:36.236000", - "last_modified_date": "2024-08-16 23:58:36.236000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "ff81bcf6-b368-4264-872c-544e85ec80e8", - "volume_id": null - }, - { - "id": "e0fd5afe-9d21-4baf-bf93-23c628f8b246", - "created_date": "2024-08-16 23:58:36.362000", - "last_modified_date": "2024-08-16 23:58:36.362000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "c91ec109-0b4d-4fd6-995f-1a828958493f", - "volume_id": null - }, - { - "id": "e1125ccb-d440-4c85-8677-fd5347f74147", - "created_date": "2024-08-16 23:58:36.550000", - "last_modified_date": "2024-08-16 23:58:36.550000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "bf59f643-455e-4b60-95b8-719d55437474", - "volume_id": null - }, - { - "id": "e16418ed-65c3-4925-9bec-8bef8732ab52", - "created_date": "2024-08-16 23:58:36.698000", - "last_modified_date": "2024-08-16 23:58:36.698000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "152", - "comic_id": "cc96b25e-b827-4ff0-a94a-b82d30ca883c", - "volume_id": null - }, - { - "id": "e191bee5-85dc-43d4-9ade-d8c006a93dcc", - "created_date": "2024-08-16 23:58:35.807000", - "last_modified_date": "2024-08-16 23:58:35.807000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "89c5ea13-997a-4831-87cc-ee76ea05c71e", - "volume_id": null - }, - { - "id": "e1d76830-7277-48d9-b2af-c74f1248b5c5", - "created_date": "2024-08-16 23:58:35.632000", - "last_modified_date": "2024-08-16 23:58:35.632000", - "version": 0, - "in_stock": 0, - "is_read": 1, - "issue_number": "8", - "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", - "volume_id": null - }, - { - "id": "e21ce1d0-7349-4a93-943f-4fd41bbdd175", - "created_date": "2024-08-16 23:58:35.778000", - "last_modified_date": "2024-08-16 23:58:35.778000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "5a72a63d-8fbd-46a8-b201-9b6e035c782a", - "volume_id": null - }, - { - "id": "e305c4a5-37a4-41e4-8c58-b5b065e4fdf5", - "created_date": "2024-08-16 23:58:36.720000", - "last_modified_date": "2024-08-16 23:58:36.720000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "25841b05-c246-4484-9ef2-71f8dcdb39ed", - "volume_id": null - }, - { - "id": "e3728535-ffcd-47e0-ab3c-e26cb8d1f214", - "created_date": "2024-08-16 23:58:36.663000", - "last_modified_date": "2024-08-16 23:58:36.663000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "c9d4b26a-8431-4f68-8e7a-b61f2cf24176", - "volume_id": null - }, - { - "id": "e37d3491-8104-46e1-a3e2-f3f0e5bd648d", - "created_date": "2024-08-16 23:58:36.705000", - "last_modified_date": "2024-08-16 23:58:36.705000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "a2409ef1-82c3-45d1-9c65-b8806a31e525", - "volume_id": null - }, - { - "id": "e38f4fc3-7823-478d-a5bc-62dfe82e36d3", - "created_date": "2024-08-16 23:58:36.641000", - "last_modified_date": "2024-08-16 23:58:36.641000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "102", - "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", - "volume_id": null - }, - { - "id": "e3cb2b41-bba4-40a6-a8d9-b4ce1b79a4d6", - "created_date": "2024-08-16 23:58:36.485000", - "last_modified_date": "2024-08-16 23:58:36.485000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "e3dd5af3-1c7c-4391-971b-f13346957c00", - "created_date": "2024-08-16 23:58:36.480000", - "last_modified_date": "2024-08-16 23:58:36.480000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "35", - "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", - "volume_id": null - }, - { - "id": "e41501fa-cb58-4945-ad0e-1faf3be4bb68", - "created_date": "2024-08-16 23:58:36.838000", - "last_modified_date": "2024-08-16 23:58:36.838000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "9fcdd9a5-f1fb-4421-a352-9da8e2c12f81", - "volume_id": null - }, - { - "id": "e493d4c5-7d93-49a2-b8a0-9b9177ee91e3", - "created_date": "2024-08-16 23:58:36.195000", - "last_modified_date": "2024-08-16 23:58:36.195000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "efc52177-93a0-4e69-a76f-2c9049ff3967", - "volume_id": null - }, - { - "id": "e4cda530-52c5-4ead-9723-3c277c484cba", - "created_date": "2024-08-16 23:58:36.886000", - "last_modified_date": "2024-08-16 23:58:36.886000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "10", - "comic_id": "04d29010-17a4-4ad6-a73a-740b487a4ecc", - "volume_id": null - }, - { - "id": "e54e71be-d5fe-4de7-9ad7-ebc3156b578c", - "created_date": "2024-08-16 23:58:36.624000", - "last_modified_date": "2024-08-16 23:58:36.624000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "1d9440ba-202a-4c4e-8425-8561606c1c3e", - "volume_id": null - }, - { - "id": "e5701dab-a038-4bee-9086-602c6848cc39", - "created_date": "2024-08-16 23:58:36.230000", - "last_modified_date": "2024-08-16 23:58:36.230000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "fe7fb34e-5ff2-4f6d-a649-0be19a368f46", - "volume_id": null - }, - { - "id": "e645cbc3-bdb2-4278-bcc2-d4412b9de60f", - "created_date": "2024-08-16 23:58:35.653000", - "last_modified_date": "2024-08-16 23:58:35.653000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", - "volume_id": null - }, - { - "id": "e6649305-1c25-46c2-a030-e8b067627c0f", - "created_date": "2024-08-16 23:58:36.410000", - "last_modified_date": "2024-08-16 23:58:36.410000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "48", - "comic_id": "57965b27-1330-4921-8c0b-4b09ee06084f", - "volume_id": null - }, - { - "id": "e66f1ea0-65ed-4f8a-8488-f3aec2dca7b0", - "created_date": "2024-08-16 23:58:36.330000", - "last_modified_date": "2024-08-16 23:58:36.330000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "7c110b15-bbfc-472e-830b-b8db6ddb274e", - "volume_id": null - }, - { - "id": "e71fd5dc-15b3-442b-8241-8adfe9abcde3", - "created_date": "2024-08-16 23:58:36.832000", - "last_modified_date": "2024-08-16 23:58:36.832000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "8e293af3-05c6-4dcc-9cbf-b87512ec975b", - "volume_id": null - }, - { - "id": "e7920076-289a-4ebf-8ca7-4b54c04c7ed5", - "created_date": "2024-08-16 23:58:35.611000", - "last_modified_date": "2024-08-16 23:58:35.611000", - "version": 0, - "in_stock": 0, - "is_read": 1, - "issue_number": "1", - "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369", - "volume_id": null - }, - { - "id": "e7ff7168-4229-4456-ad2a-b2ef5420bc2c", - "created_date": "2024-08-16 23:58:35.851000", - "last_modified_date": "2024-08-16 23:58:35.851000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "11", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "e893ea6d-50c8-4d53-963a-920b4bd41a9d", - "created_date": "2024-08-16 23:58:37.021000", - "last_modified_date": "2024-08-16 23:58:37.021000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "513", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "e8a565ce-da7c-43b3-b433-a6bebdac4641", - "created_date": "2024-08-16 23:58:36.215000", - "last_modified_date": "2024-08-16 23:58:36.215000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "10", - "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", - "volume_id": null - }, - { - "id": "e9501fdb-6b64-459a-a37d-452b7b528be7", - "created_date": "2024-08-16 23:58:36.900000", - "last_modified_date": "2024-08-16 23:58:36.900000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "799309bc-d8d9-4d44-9457-bae7f1e7fcbf", - "volume_id": null - }, - { - "id": "e95a3246-4e9b-414a-8f22-3e727535b6a4", - "created_date": "2024-08-16 23:58:36.937000", - "last_modified_date": "2024-08-16 23:58:36.937000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "40", - "comic_id": "2a4b287e-4b05-4016-8eb4-21fc225be24d", - "volume_id": null - }, - { - "id": "e96fa441-7085-4c40-acc8-871f947868f4", - "created_date": "2024-08-16 23:58:36.479000", - "last_modified_date": "2024-08-16 23:58:36.479000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "34", - "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", - "volume_id": null - }, - { - "id": "e979103a-3655-43a6-8047-af3967fe3273", - "created_date": "2024-08-16 23:58:36.367000", - "last_modified_date": "2024-08-16 23:58:36.367000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "0", - "comic_id": "b6713944-8d2d-4153-8f16-fe94cc4ee119", - "volume_id": null - }, - { - "id": "e9964166-fd4c-41bf-b3e2-5f5d0fdd9365", - "created_date": "2024-08-16 23:58:35.888000", - "last_modified_date": "2024-08-16 23:58:35.888000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "23", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "e9a24aeb-e5cc-458a-bd9b-2379ae331df7", - "created_date": "2024-08-16 23:58:36.953000", - "last_modified_date": "2024-08-16 23:58:36.953000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "d33657a6-0ab7-4177-b5b3-4a0c9d358d03", - "volume_id": null - }, - { - "id": "ea29f973-91a6-4056-a4c4-3851b8552202", - "created_date": "2024-08-16 23:58:36.979000", - "last_modified_date": "2024-08-16 23:58:36.979000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "13", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "eaba8699-b092-460d-be8b-326222e02ec8", - "created_date": "2024-08-16 23:58:36.556000", - "last_modified_date": "2024-08-16 23:58:36.556000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "100b82bf-c134-40d9-bcc2-a8b345030b8d", - "volume_id": null - }, - { - "id": "eb3c9af0-7c94-4020-83ea-40e923e767ae", - "created_date": "2024-08-16 23:58:36.545000", - "last_modified_date": "2024-08-16 23:58:36.545000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "bf59f643-455e-4b60-95b8-719d55437474", - "volume_id": null - }, - { - "id": "ebd68581-fe3c-40ce-bd1a-c64960a65d78", - "created_date": "2024-08-16 23:58:36.913000", - "last_modified_date": "2024-08-16 23:58:36.913000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "a08aef89-634c-494c-9def-73ff7e416464", - "volume_id": null - }, - { - "id": "ebebecc2-b31c-43ee-9d7b-30048356b132", - "created_date": "2024-08-16 23:58:36.427000", - "last_modified_date": "2024-08-16 23:58:36.427000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "83", - "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", - "volume_id": null - }, - { - "id": "ede115b0-ae5e-4fd5-ba19-5e2ad56ddb8b", - "created_date": "2024-08-16 23:58:36.684000", - "last_modified_date": "2024-08-16 23:58:36.684000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "10", - "comic_id": "0654be4e-49e7-4fb4-b9b5-77d0f807a1ca", - "volume_id": null - }, - { - "id": "ee804dc0-58b8-46b3-a9d7-904030506bdc", - "created_date": "2024-08-16 23:58:36.441000", - "last_modified_date": "2024-08-16 23:58:36.441000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "92", - "comic_id": "8585e73a-f94a-43e2-8204-02a2e3d364c4", - "volume_id": null - }, - { - "id": "eeb2ad49-a1c3-4d41-b63f-8e8ae7b271fe", - "created_date": "2024-08-16 23:58:36.148000", - "last_modified_date": "2024-08-16 23:58:36.148000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "7", - "comic_id": "c0543c9f-d712-4dce-9b59-2bf73b800b31", - "volume_id": null - }, - { - "id": "eeb4499e-7a67-469f-9672-44081a35f651", - "created_date": "2024-08-16 23:58:36.152000", - "last_modified_date": "2024-08-16 23:58:36.152000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "5fc6600f-b005-4d6b-a1af-a17cc2701d81", - "volume_id": null - }, - { - "id": "eed2495c-cf50-47d2-9ceb-23ebf4174b48", - "created_date": "2024-08-16 23:58:36.796000", - "last_modified_date": "2024-08-16 23:58:36.796000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "10", - "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", - "volume_id": null - }, - { - "id": "ef0e647d-cfc2-4fbe-a09e-5ea454b381f2", - "created_date": "2024-08-16 23:58:36.548000", - "last_modified_date": "2024-08-16 23:58:36.548000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "bf59f643-455e-4b60-95b8-719d55437474", - "volume_id": null - }, - { - "id": "f04e696e-674c-4dcd-ab12-bb8897b553dd", - "created_date": "2024-08-16 23:58:36.396000", - "last_modified_date": "2024-08-16 23:58:36.396000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "214", - "comic_id": "d24801e2-fbfe-4497-873f-4d8edb182ae4", - "volume_id": null - }, - { - "id": "f058b0f7-a0e1-4aae-a4e3-2f4d0ff64435", - "created_date": "2024-08-16 23:58:35.786000", - "last_modified_date": "2024-08-16 23:58:35.786000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "5a72a63d-8fbd-46a8-b201-9b6e035c782a", - "volume_id": null - }, - { - "id": "f05f4330-6428-426d-ab61-86ca69054497", - "created_date": "2024-08-16 23:58:36.831000", - "last_modified_date": "2024-08-16 23:58:36.831000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "8e293af3-05c6-4dcc-9cbf-b87512ec975b", - "volume_id": null - }, - { - "id": "f104ab76-8013-458e-aff4-6390ba9c15db", - "created_date": "2024-08-16 23:58:37.114000", - "last_modified_date": "2024-08-16 23:58:37.114000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "ea903b99-4032-4b4e-add4-177e051733a8", - "volume_id": null - }, - { - "id": "f1327ef4-975d-44c3-8b46-c5640789fa0e", - "created_date": "2024-08-16 23:58:36.539000", - "last_modified_date": "2024-08-16 23:58:36.539000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "f4cb5b24-00ea-4249-9d09-45432c168b8f", - "volume_id": null - }, - { - "id": "f14a6885-22b7-4495-bb22-3f95d64e9a97", - "created_date": "2024-08-16 23:58:36.967000", - "last_modified_date": "2024-08-16 23:58:36.967000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "f1622875-aa1c-4d96-91c1-7aaabf474394", - "created_date": "2024-08-16 23:58:36.309000", - "last_modified_date": "2024-08-16 23:58:36.309000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "60deca87-7b2a-4412-855f-01a6ccaeea56", - "volume_id": null - }, - { - "id": "f1e0321f-629b-49dc-bb92-dfcf34814e9e", - "created_date": "2024-08-16 23:58:36.713000", - "last_modified_date": "2024-08-16 23:58:36.713000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "e1ff1410-ac3a-4ba5-8503-1fab529e50c0", - "volume_id": null - }, - { - "id": "f1fe0246-06f5-4f06-b4a4-83bb823f775e", - "created_date": "2024-08-16 23:58:35.994000", - "last_modified_date": "2024-08-16 23:58:35.994000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "4b883248-716e-45b9-be2a-1eab276159bb", - "volume_id": null - }, - { - "id": "f2596e0c-7b20-47f8-97e8-5976429ddae0", - "created_date": "2024-08-16 23:58:36.095000", - "last_modified_date": "2024-08-16 23:58:36.095000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "46d02138-9fb9-4fbe-9928-d73c0faf2a4e", - "volume_id": null - }, - { - "id": "f3107ebd-45e5-43b8-a126-70f2c068ea61", - "created_date": "2024-08-16 23:58:36.221000", - "last_modified_date": "2024-08-16 23:58:36.221000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "12", - "comic_id": "6f341583-4a3b-4d9a-b928-124f024fa005", - "volume_id": null - }, - { - "id": "f319368c-010d-4084-946a-f1e508a50a40", - "created_date": "2024-08-16 23:58:35.715000", - "last_modified_date": "2024-08-16 23:58:35.715000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "14", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "f3613c94-48a1-4e08-b4a7-fef7f3760656", - "created_date": "2024-08-16 23:58:35.955000", - "last_modified_date": "2024-08-16 23:58:35.955000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "13", - "comic_id": "1b52609a-4c4c-4326-a373-5e7836c5d3b3", - "volume_id": null - }, - { - "id": "f40f0239-9612-4fed-8563-0da590ab4ef7", - "created_date": "2024-08-16 23:58:36.554000", - "last_modified_date": "2024-08-16 23:58:36.554000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "c686b9c2-abdc-4476-a530-ee85ce221a5d", - "volume_id": null - }, - { - "id": "f4b58c38-4096-4851-9557-9ef709e6d702", - "created_date": "2024-08-16 23:58:36.122000", - "last_modified_date": "2024-08-16 23:58:36.122000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "17b50be7-aca7-446e-8729-3706f636d29d", - "volume_id": null - }, - { - "id": "f51be125-6e35-43d6-8bc6-a569bcd599eb", - "created_date": "2024-08-16 23:58:36.114000", - "last_modified_date": "2024-08-16 23:58:36.114000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "0f0bfd13-f6f0-436c-ad74-5e40ea6a0cf8", - "volume_id": null - }, - { - "id": "f579c1aa-a23c-4892-9eba-d5921ddad67e", - "created_date": "2024-08-16 23:58:35.890000", - "last_modified_date": "2024-08-16 23:58:35.890000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "24", - "comic_id": "c09071ad-5171-450a-92ce-e1055d6f65da", - "volume_id": null - }, - { - "id": "f5a56360-bea8-4b1a-9226-809efbd3b03b", - "created_date": "2024-08-16 23:58:35.647000", - "last_modified_date": "2024-08-16 23:58:35.647000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "9a175907-fea8-4f11-903f-6e837ce666c0", - "volume_id": null - }, - { - "id": "f5b790f6-34e4-4751-8c5d-b4d667a4b289", - "created_date": "2024-08-16 23:58:36.732000", - "last_modified_date": "2024-08-16 23:58:36.732000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "fe69cf80-946a-45d6-9fba-55ebfc0f038c", - "volume_id": null - }, - { - "id": "f5f81308-6f74-4901-a601-3afb61e20138", - "created_date": "2024-08-16 23:58:36.125000", - "last_modified_date": "2024-08-16 23:58:36.125000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "f3231681-cd2b-4ff9-bfa2-5d8f631bee4d", - "volume_id": null - }, - { - "id": "f600f71a-17e0-41c0-a701-5d8df520b499", - "created_date": "2024-08-16 23:58:36.772000", - "last_modified_date": "2024-08-16 23:58:36.772000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "10", - "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27", - "volume_id": null - }, - { - "id": "f606d273-1a00-42fa-bf79-db3a162dfb3a", - "created_date": "2024-08-16 23:58:36.717000", - "last_modified_date": "2024-08-16 23:58:36.717000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "e1ff1410-ac3a-4ba5-8503-1fab529e50c0", - "volume_id": null - }, - { - "id": "f609bb6d-55ed-4c93-8ea7-818f3a16c283", - "created_date": "2024-08-16 23:58:36.336000", - "last_modified_date": "2024-08-16 23:58:36.336000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", - "volume_id": null - }, - { - "id": "f62a7bd0-3c5f-4460-8a99-9d6ac10d2bfb", - "created_date": "2024-08-16 23:58:36.267000", - "last_modified_date": "2024-08-16 23:58:36.267000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "5d0fd720-7875-4f9f-86eb-07ee0723908f", - "volume_id": null - }, - { - "id": "f692a0e8-64bd-4f73-af9c-8549f13f9aa4", - "created_date": "2024-08-16 23:58:36.815000", - "last_modified_date": "2024-08-16 23:58:36.815000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "12802b17-452a-4533-a7ab-fd94f19be123", - "volume_id": null - }, - { - "id": "f6aebd74-9c70-46fb-b820-07ed07d7e823", - "created_date": "2024-08-16 23:58:37.113000", - "last_modified_date": "2024-08-16 23:58:37.113000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "2ab84282-d7f5-43ef-af41-df0f756a62f7", - "volume_id": null - }, - { - "id": "f6bdfb93-f196-4f01-a112-c80d9b93dde5", - "created_date": "2024-08-16 23:58:36.963000", - "last_modified_date": "2024-08-16 23:58:36.963000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "f72781ea-72f2-4f04-98b3-4f47172ec908", - "created_date": "2024-08-16 23:58:36.801000", - "last_modified_date": "2024-08-16 23:58:36.801000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "13", - "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", - "volume_id": null - }, - { - "id": "f769e448-19d1-4c38-89c3-e6ffb9bf340f", - "created_date": "2024-08-16 23:58:36.955000", - "last_modified_date": "2024-08-16 23:58:36.955000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "d33657a6-0ab7-4177-b5b3-4a0c9d358d03", - "volume_id": null - }, - { - "id": "f799357e-37ab-456b-a8e0-8e9758fe67b7", - "created_date": "2024-08-16 23:58:36.639000", - "last_modified_date": "2024-08-16 23:58:36.639000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "101", - "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", - "volume_id": null - }, - { - "id": "f7c25b8a-2fbf-4c14-b393-d5b9baf3fb0e", - "created_date": "2024-08-16 23:58:37.099000", - "last_modified_date": "2024-08-16 23:58:37.099000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "07574acf-8f77-4da7-87fd-c49f40536baf", - "volume_id": null - }, - { - "id": "f8566ced-aeec-47b3-9571-af8e406f0249", - "created_date": "2024-08-16 23:58:36.186000", - "last_modified_date": "2024-08-16 23:58:36.186000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "10", - "comic_id": "e2e7a53a-fbd5-473c-9409-3acb87247728", - "volume_id": null - }, - { - "id": "f883f05f-b815-4c74-bdca-12af771eef44", - "created_date": "2024-08-16 23:58:36.301000", - "last_modified_date": "2024-08-16 23:58:36.301000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "13281ef7-5945-49a9-b8f1-c5e8548c18ec", - "volume_id": null - }, - { - "id": "f8e1a631-262a-455c-9efc-cfdde559a123", - "created_date": "2024-08-16 23:58:36.057000", - "last_modified_date": "2024-08-16 23:58:36.057000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "6", - "comic_id": "b472b359-d586-458c-9042-a5fee057da3b", - "volume_id": null - }, - { - "id": "f9159a59-34a0-4562-aef3-47eb7272fc77", - "created_date": "2024-08-16 23:58:36.083000", - "last_modified_date": "2024-08-16 23:58:36.083000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "11bc20d5-bfeb-4825-9e0f-3d6954020b07", - "volume_id": null - }, - { - "id": "f93c91e5-6f6f-4668-aaad-81bab3eb7253", - "created_date": "2024-08-16 23:58:35.982000", - "last_modified_date": "2024-08-16 23:58:35.982000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "10", - "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", - "volume_id": null - }, - { - "id": "f9481e79-639f-4b55-84b0-d4c49656792c", - "created_date": "2024-08-16 23:58:36.311000", - "last_modified_date": "2024-08-16 23:58:36.311000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "60deca87-7b2a-4412-855f-01a6ccaeea56", - "volume_id": null - }, - { - "id": "f978ee04-9b37-4d3f-8ce5-4c69a9b014e3", - "created_date": "2024-08-16 23:58:36.735000", - "last_modified_date": "2024-08-16 23:58:36.735000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "8690631a-e99c-44be-887c-8fd2bb222ee1", - "volume_id": null - }, - { - "id": "f9dd23f9-aa3b-4667-824e-eea1915060b7", - "created_date": "2024-08-16 23:58:36.451000", - "last_modified_date": "2024-08-16 23:58:36.451000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "23", - "comic_id": "2f7e2850-b51e-4161-9ef6-36eed26a113b", - "volume_id": null - }, - { - "id": "f9f1c40e-e163-4691-ac2f-9a1ae219cb82", - "created_date": "2024-08-16 23:58:36.578000", - "last_modified_date": "2024-08-16 23:58:36.578000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "0095a769-e5e2-441e-8a38-f98743ee9529", - "volume_id": null - }, - { - "id": "fa05766f-3834-4e94-8598-f55ed2db08b4", - "created_date": "2024-08-16 23:58:36.637000", - "last_modified_date": "2024-08-16 23:58:36.637000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "100", - "comic_id": "f3043f7c-8039-4b83-98ad-c2d088c9e291", - "volume_id": null - }, - { - "id": "fa5a93d9-c292-44ce-8be8-507d88198c7f", - "created_date": "2024-08-16 23:58:35.699000", - "last_modified_date": "2024-08-16 23:58:35.699000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "8", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "fa7aec1c-dbbf-4ea0-a99e-539d98464f85", - "created_date": "2024-08-16 23:58:36.235000", - "last_modified_date": "2024-08-16 23:58:36.235000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "ff81bcf6-b368-4264-872c-544e85ec80e8", - "volume_id": null - }, - { - "id": "fb104202-ef2a-4a4a-af38-8660e9331900", - "created_date": "2024-08-16 23:58:36.559000", - "last_modified_date": "2024-08-16 23:58:36.559000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "100b82bf-c134-40d9-bcc2-a8b345030b8d", - "volume_id": null - }, - { - "id": "fb5134d2-522c-4971-a333-831a33782e00", - "created_date": "2024-08-16 23:58:36.088000", - "last_modified_date": "2024-08-16 23:58:36.088000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "11bc20d5-bfeb-4825-9e0f-3d6954020b07", - "volume_id": null - }, - { - "id": "fc4f9cd5-6666-4576-9faa-642271aff2b9", - "created_date": "2024-08-16 23:58:37.016000", - "last_modified_date": "2024-08-16 23:58:37.016000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "510", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261", - "volume_id": null - }, - { - "id": "fd3bf188-4b5b-4996-9f9a-29c3aa04df36", - "created_date": "2024-08-16 23:58:36.798000", - "last_modified_date": "2024-08-16 23:58:36.798000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "11", - "comic_id": "63cfc38f-5f4e-4273-a630-7b455868687b", - "volume_id": null - }, - { - "id": "fd50d8e7-31c3-4dd1-bfc3-62dc55b23005", - "created_date": "2024-08-16 23:58:36.374000", - "last_modified_date": "2024-08-16 23:58:36.374000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "b6713944-8d2d-4153-8f16-fe94cc4ee119", - "volume_id": null - }, - { - "id": "fdabc2b4-b90c-49c2-9175-ba255c3a4a59", - "created_date": "2024-08-16 23:58:36.843000", - "last_modified_date": "2024-08-16 23:58:36.843000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "618d7dba-9ae2-4cd1-bc0c-8652863d1f69", - "volume_id": null - }, - { - "id": "fdcaf3b8-a458-46f8-a3b6-560dd94e44aa", - "created_date": "2024-08-16 23:58:36.985000", - "last_modified_date": "2024-08-16 23:58:36.985000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "17", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "fdf07c29-e365-459e-8842-d8175ea5b810", - "created_date": "2024-08-16 23:58:36.035000", - "last_modified_date": "2024-08-16 23:58:36.035000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "bf23d317-f5b6-4cf8-8a05-4397888a82c9", - "volume_id": null - }, - { - "id": "fe0213ea-65a4-4995-ad65-a37244c0ce59", - "created_date": "2024-08-16 23:58:36.514000", - "last_modified_date": "2024-08-16 23:58:36.514000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "20", - "comic_id": "a2015c25-fa16-4578-900e-d0aeb4a6c4d6", - "volume_id": null - }, - { - "id": "fe27df42-d815-4f43-b764-37b6ad2b723a", - "created_date": "2024-08-16 23:58:35.971000", - "last_modified_date": "2024-08-16 23:58:35.971000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "4", - "comic_id": "5f648121-c503-46df-8a2b-56c11f5be6b4", - "volume_id": null - }, - { - "id": "fe38f460-f857-435c-aa88-7bc58abe59fb", - "created_date": "2024-08-16 23:58:36.965000", - "last_modified_date": "2024-08-16 23:58:36.965000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "5", - "comic_id": "98be937b-12d0-4f7e-842c-d925cac13d04", - "volume_id": null - }, - { - "id": "fe628193-48d4-463e-b850-875818fe596f", - "created_date": "2024-08-16 23:58:36.574000", - "last_modified_date": "2024-08-16 23:58:36.574000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "0095a769-e5e2-441e-8a38-f98743ee9529", - "volume_id": null - }, - { - "id": "fed14271-c3dc-4b46-999e-16bf238ddbbb", - "created_date": "2024-08-16 23:58:35.736000", - "last_modified_date": "2024-08-16 23:58:35.736000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "21", - "comic_id": "b6e7b156-b7ac-4b8a-8d1d-23c234a7b015", - "volume_id": null - }, - { - "id": "fee4ab2b-ae91-4df1-996e-5603321ef571", - "created_date": "2024-08-16 23:58:36.894000", - "last_modified_date": "2024-08-16 23:58:36.894000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "0fd9969e-8e1f-4f42-94ac-fab4d2d614c2", - "volume_id": null - }, - { - "id": "ff6136be-6e6d-4f71-9621-b5810fba6790", - "created_date": "2024-08-16 23:58:36.747000", - "last_modified_date": "2024-08-16 23:58:36.747000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "3", - "comic_id": "fffaa1a6-5c3d-4deb-b759-35de74c65958", - "volume_id": null - }, - { - "id": "ff869dfc-edae-4cc9-bbf7-ec53e79f0a19", - "created_date": "2024-08-16 23:58:37.089000", - "last_modified_date": "2024-08-16 23:58:37.089000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "14a359a2-c928-4e14-9fb2-249777e6a13a", - "volume_id": null - }, - { - "id": "ff9c913d-7c2d-4805-aef4-8b0184c9ebe7", - "created_date": "2024-08-16 23:58:36.580000", - "last_modified_date": "2024-08-16 23:58:36.580000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "1", - "comic_id": "8955b2a3-84d3-4b55-a1f1-d45193600861", - "volume_id": null - }, - { - "id": "ffa94296-9736-4d2d-8447-202e137b970b", - "created_date": "2024-08-16 23:58:36.334000", - "last_modified_date": "2024-08-16 23:58:36.334000", - "version": 0, - "in_stock": 0, - "is_read": 0, - "issue_number": "2", - "comic_id": "af8ae2a5-7652-460f-8f2b-6543f6ab0fc4", - "volume_id": null - } - ], - "book_author": [], - "trade_paperback": [ - { - "id": "02386448-b7b0-4ae2-92c9-1db4fb1ef51b", - "created_date": "2024-08-16 23:58:35.576000", - "last_modified_date": "2024-08-16 23:58:35.576000", - "version": 0, - "issue_start": 40, - "issue_end": 45, - "name": "Until the Stars Turn Cold", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261" - }, - { - "id": "05e79848-fdcb-4914-82bd-26e56adfd630", - "created_date": "2024-08-16 23:58:35.533000", - "last_modified_date": "2024-08-16 23:58:35.533000", - "version": 0, - "issue_start": 8, - "issue_end": 14, - "name": "Blood For Blood", - "comic_id": "8ad36a79-9436-455d-8096-0f1b73c22f13" - }, - { - "id": "1a58e34c-7048-4882-bec3-57a93dda68f6", - "created_date": "2024-08-16 23:58:35.574000", - "last_modified_date": "2024-08-16 23:58:35.574000", - "version": 0, - "issue_start": 36, - "issue_end": 39, - "name": "Revelations", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261" - }, - { - "id": "1e9d193a-743c-43df-a202-0e1e159528ee", - "created_date": "2024-08-16 23:58:35.490000", - "last_modified_date": "2024-08-16 23:58:35.490000", - "version": 0, - "issue_start": 1, - "issue_end": 18, - "name": "Vol. 1", - "comic_id": "134659ae-67a4-4cad-aac6-36bee893102f" - }, - { - "id": "203ad009-5467-4df3-b543-14f167d43556", - "created_date": "2024-08-16 23:58:35.581000", - "last_modified_date": "2024-08-16 23:58:35.581000", - "version": 0, - "issue_start": 51, - "issue_end": 56, - "name": "Unintended Consequences", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261" - }, - { - "id": "247b0218-7554-4d9c-be9d-7cdc44c34670", - "created_date": "2024-08-16 23:58:35.508000", - "last_modified_date": "2024-08-16 23:58:35.508000", - "version": 0, - "issue_start": 15, - "issue_end": 20, - "name": "Taking The Skies", - "comic_id": "2a4b287e-4b05-4016-8eb4-21fc225be24d" - }, - { - "id": "24a0194c-bc81-4915-9270-8df6d0362bd8", - "created_date": "2024-08-16 23:58:35.492000", - "last_modified_date": "2024-08-16 23:58:35.492000", - "version": 0, - "issue_start": 1, - "issue_end": 5, - "name": "Choices", - "comic_id": "5d2f3bf7-da3e-47a4-b475-aab03d073e27" - }, - { - "id": "264b7882-653e-4dc1-821a-2803e8e6c742", - "created_date": "2024-08-16 23:58:35.503000", - "last_modified_date": "2024-08-16 23:58:35.503000", - "version": 0, - "issue_start": 1, - "issue_end": 7, - "name": "Flying Solo", - "comic_id": "2a4b287e-4b05-4016-8eb4-21fc225be24d" - }, - { - "id": "294c0f2c-5a2b-46d3-939f-2dc6c2f8a82c", - "created_date": "2024-08-16 23:58:35.583000", - "last_modified_date": "2024-08-16 23:58:35.583000", - "version": 0, - "issue_start": 500, - "issue_end": 502, - "name": "Happy Birthday", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261" - }, - { - "id": "29cfdd9a-9d7b-4c22-a291-c6dc3d71fd9a", - "created_date": "2024-08-16 23:58:35.558000", - "last_modified_date": "2024-08-16 23:58:35.558000", - "version": 0, - "issue_start": 13, - "issue_end": 18, - "name": "Earth Angel", - "comic_id": "56152b4f-9a84-40ea-a329-8a267d931182" - }, - { - "id": "2fc730b6-bd70-4f5c-8d33-e43c97115a39", - "created_date": "2024-08-16 23:58:35.510000", - "last_modified_date": "2024-08-16 23:58:35.510000", - "version": 0, - "issue_start": 21, - "issue_end": 26, - "name": "Coming Home", - "comic_id": "2a4b287e-4b05-4016-8eb4-21fc225be24d" - }, - { - "id": "327e1360-b942-4559-b187-90f9032d28f4", - "created_date": "2024-08-16 23:58:35.527000", - "last_modified_date": "2024-08-16 23:58:35.527000", - "version": 0, - "issue_start": 7, - "issue_end": 12, - "name": "Superhuman Law", - "comic_id": "e54ebebe-701a-4d60-88ce-4df9b34da6ca" - }, - { - "id": "362a4300-e0fb-4092-8f74-d1605c1541a8", - "created_date": "2024-08-16 23:58:35.483000", - "last_modified_date": "2024-08-16 23:58:35.483000", - "version": 0, - "issue_start": 13, - "issue_end": 18, - "name": "The Warriors Tale", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc" - }, - { - "id": "38feab7c-dc44-4a25-8cad-c72727551e84", - "created_date": "2024-08-16 23:58:35.530000", - "last_modified_date": "2024-08-16 23:58:35.530000", - "version": 0, - "issue_start": 1, - "issue_end": 7, - "name": "Conflict of Conscience", - "comic_id": "8ad36a79-9436-455d-8096-0f1b73c22f13" - }, - { - "id": "3a524877-b2fc-4d3c-b23e-df0b565e9322", - "created_date": "2024-08-16 23:58:35.546000", - "last_modified_date": "2024-08-16 23:58:35.546000", - "version": 0, - "issue_start": 1, - "issue_end": 6, - "name": "Public Enemies", - "comic_id": "ed58b16e-0701-4373-befe-39118bc2d4cb" - }, - { - "id": "58dd3153-ce56-4b33-92d9-3e7b2d27f79f", - "created_date": "2024-08-16 23:58:35.500000", - "last_modified_date": "2024-08-16 23:58:35.500000", - "version": 0, - "issue_start": 13, - "issue_end": 18, - "name": "Strangers in Atlantis", - "comic_id": "2ca8751f-8d0a-449e-933f-f293e6fbd751" - }, - { - "id": "5ae999a1-9863-4caa-9c7a-f5bf3338344d", - "created_date": "2024-08-16 23:58:35.554000", - "last_modified_date": "2024-08-16 23:58:35.554000", - "version": 0, - "issue_start": 7, - "issue_end": 12, - "name": "Heaven & Earth", - "comic_id": "56152b4f-9a84-40ea-a329-8a267d931182" - }, - { - "id": "62a262e5-a284-472c-8617-cb314164c61e", - "created_date": "2024-08-16 23:58:35.566000", - "last_modified_date": "2024-08-16 23:58:35.566000", - "version": 0, - "issue_start": 1, - "issue_end": 8, - "name": "1602", - "comic_id": "94837af3-bbaf-4496-b114-1676a3271cb6" - }, - { - "id": "74e93758-8c2a-4e5d-9d1c-f69835a439ea", - "created_date": "2024-08-16 23:58:35.524000", - "last_modified_date": "2024-08-16 23:58:35.524000", - "version": 0, - "issue_start": 1, - "issue_end": 6, - "name": "Single Green Female", - "comic_id": "e54ebebe-701a-4d60-88ce-4df9b34da6ca" - }, - { - "id": "8d1293f9-3f8b-4cc1-8a66-25799dbe2208", - "created_date": "2024-08-16 23:58:35.520000", - "last_modified_date": "2024-08-16 23:58:35.520000", - "version": 0, - "issue_start": 21, - "issue_end": 26, - "name": "Out All Night", - "comic_id": "1b7de491-6bbb-404e-a2e5-a20a123e3fe5" - }, - { - "id": "8e0228ef-8d67-4475-befb-04745ef571ac", - "created_date": "2024-08-16 23:58:35.486000", - "last_modified_date": "2024-08-16 23:58:35.486000", - "version": 0, - "issue_start": 19, - "issue_end": 24, - "name": "The Thiefs Tale", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc" - }, - { - "id": "9a6436f7-53ad-43d3-871f-e727cb757391", - "created_date": "2024-08-16 23:58:35.562000", - "last_modified_date": "2024-08-16 23:58:35.562000", - "version": 0, - "issue_start": 19, - "issue_end": 24, - "name": "Redemption", - "comic_id": "56152b4f-9a84-40ea-a329-8a267d931182" - }, - { - "id": "9b9bac86-8be9-4d9d-82a7-1fe69dc101a9", - "created_date": "2024-08-16 23:58:35.479000", - "last_modified_date": "2024-08-16 23:58:35.479000", - "version": 0, - "issue_start": 7, - "issue_end": 12, - "name": "The Dragons Tale", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc" - }, - { - "id": "a5993845-f7e2-4996-a9b7-99e79f972d61", - "created_date": "2024-08-16 23:58:35.505000", - "last_modified_date": "2024-08-16 23:58:35.505000", - "version": 0, - "issue_start": 8, - "issue_end": 14, - "name": "Going To Ground", - "comic_id": "2a4b287e-4b05-4016-8eb4-21fc225be24d" - }, - { - "id": "b957bf23-07df-439c-a4f4-7529601438c0", - "created_date": "2024-08-16 23:58:35.512000", - "last_modified_date": "2024-08-16 23:58:35.512000", - "version": 0, - "issue_start": 1, - "issue_end": 7, - "name": "Rite of Passage", - "comic_id": "1b7de491-6bbb-404e-a2e5-a20a123e3fe5" - }, - { - "id": "bda23863-2c95-4fd4-8486-2f514b70920e", - "created_date": "2024-08-16 23:58:35.472000", - "last_modified_date": "2024-08-16 23:58:35.472000", - "version": 0, - "issue_start": 1, - "issue_end": 12, - "name": "Vol. 1", - "comic_id": "f49085fd-c407-4aa8-bc57-118dde083369" - }, - { - "id": "beb2a1eb-1c40-45f7-a0ff-6f515821beca", - "created_date": "2024-08-16 23:58:35.587000", - "last_modified_date": "2024-08-16 23:58:35.587000", - "version": 0, - "issue_start": 56, - "issue_end": 61, - "name": "Of Like Minds", - "comic_id": "4a5c0ca1-9a08-49b9-bf7c-7bde4f35551a" - }, - { - "id": "c11cad0a-8501-4072-af1c-66ad28a08a71", - "created_date": "2024-08-16 23:58:35.578000", - "last_modified_date": "2024-08-16 23:58:35.578000", - "version": 0, - "issue_start": 46, - "issue_end": 50, - "name": "The Life & Death of Spiders", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261" - }, - { - "id": "ca73f9cf-112b-46e6-907f-adba5e9f9327", - "created_date": "2024-08-16 23:58:35.476000", - "last_modified_date": "2024-08-16 23:58:35.476000", - "version": 0, - "issue_start": 1, - "issue_end": 6, - "name": "From The Ashes", - "comic_id": "9d9418be-7a50-4f1b-84d6-151d5b6b74fc" - }, - { - "id": "cb129693-5418-4e2f-b2d6-f59445155f02", - "created_date": "2024-08-16 23:58:35.497000", - "last_modified_date": "2024-08-16 23:58:35.497000", - "version": 0, - "issue_start": 7, - "issue_end": 12, - "name": "Test Of Time", - "comic_id": "2ca8751f-8d0a-449e-933f-f293e6fbd751" - }, - { - "id": "d092c5be-a1ed-42bc-8ccc-110515af2122", - "created_date": "2024-08-16 23:58:35.550000", - "last_modified_date": "2024-08-16 23:58:35.550000", - "version": 0, - "issue_start": 1, - "issue_end": 6, - "name": "Loyalty And Loss", - "comic_id": "56152b4f-9a84-40ea-a329-8a267d931182" - }, - { - "id": "d1f8a566-4d12-4f2e-b1be-6024f54e517e", - "created_date": "2024-08-16 23:58:35.495000", - "last_modified_date": "2024-08-16 23:58:35.495000", - "version": 0, - "issue_start": 1, - "issue_end": 6, - "name": "Atlantis Rising", - "comic_id": "2ca8751f-8d0a-449e-933f-f293e6fbd751" - }, - { - "id": "d43e483a-60ca-48b9-a6ce-ab4b171b2edb", - "created_date": "2024-08-16 23:58:35.539000", - "last_modified_date": "2024-08-16 23:58:35.539000", - "version": 0, - "issue_start": 22, - "issue_end": 27, - "name": "Sanctuary", - "comic_id": "8ad36a79-9436-455d-8096-0f1b73c22f13" - }, - { - "id": "e118cba1-0f8f-4e10-99dd-de4e28c07bb3", - "created_date": "2024-08-16 23:58:35.517000", - "last_modified_date": "2024-08-16 23:58:35.517000", - "version": 0, - "issue_start": 15, - "issue_end": 20, - "name": "Siege of Scales", - "comic_id": "1b7de491-6bbb-404e-a2e5-a20a123e3fe5" - }, - { - "id": "e5a97bc1-9d4b-4d7a-b730-6307d1894832", - "created_date": "2024-08-16 23:58:35.585000", - "last_modified_date": "2024-08-16 23:58:35.585000", - "version": 0, - "issue_start": 1, - "issue_end": 2, - "name": "Sonderband 1", - "comic_id": "03c5b145-69d4-4d7e-8323-cb2f81060829" - }, - { - "id": "e847f422-78a8-42cc-a1a4-cfcff1d86729", - "created_date": "2024-08-16 23:58:35.569000", - "last_modified_date": "2024-08-16 23:58:35.569000", - "version": 0, - "issue_start": 30, - "issue_end": 35, - "name": "Coming Home", - "comic_id": "f99cf6e7-ef68-4ece-b039-1c21f64b1261" - }, - { - "id": "f0de5b20-d172-41e1-a079-7cdf19da9f6e", - "created_date": "2024-08-16 23:58:35.590000", - "last_modified_date": "2024-08-16 23:58:35.590000", - "version": 0, - "issue_start": 62, - "issue_end": 68, - "name": "Sensei & Student", - "comic_id": "4a5c0ca1-9a08-49b9-bf7c-7bde4f35551a" - }, - { - "id": "f9daa8a2-4a33-4c38-91da-2d79f15eeac3", - "created_date": "2024-08-16 23:58:35.536000", - "last_modified_date": "2024-08-16 23:58:35.536000", - "version": 0, - "issue_start": 15, - "issue_end": 21, - "name": "Divided Loyalties", - "comic_id": "8ad36a79-9436-455d-8096-0f1b73c22f13" - }, - { - "id": "fba9cccb-2217-465c-97c7-874ae74b2cba", - "created_date": "2024-08-16 23:58:35.542000", - "last_modified_date": "2024-08-16 23:58:35.542000", - "version": 0, - "issue_start": 444, - "issue_end": 449, - "name": "The End Of History", - "comic_id": "b6383b7f-1c86-42d9-a3f6-d2a4bc96dc51" - }, - { - "id": "ffac4cc3-8111-4579-b689-a768603179a1", - "created_date": "2024-08-16 23:58:35.514000", - "last_modified_date": "2024-08-16 23:58:35.514000", - "version": 0, - "issue_start": 8, - "issue_end": 14, - "name": "The Demon Queen", - "comic_id": "1b7de491-6bbb-404e-a2e5-a20a123e3fe5" - } - ], - "mail": [] -} \ No newline at end of file From 400aff6524f0a446b743a6bb5396ec0a5b6f5da4 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Fri, 24 Jan 2025 08:58:59 +0100 Subject: [PATCH 65/91] Add MediaWindow --- python/kontor-gui/gui/main_window.py | 24 ++++++++++- python/kontor-gui/gui/media_window.py | 60 +++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 python/kontor-gui/gui/media_window.py diff --git a/python/kontor-gui/gui/main_window.py b/python/kontor-gui/gui/main_window.py index 384723b..5a97c6a 100644 --- a/python/kontor-gui/gui/main_window.py +++ b/python/kontor-gui/gui/main_window.py @@ -5,6 +5,7 @@ from sqlalchemy import Engine from kontor_schema import KontorDB from .comic_window import ComicWindow +from .media_window import MediaWindow from .progress import ProgressUpdate from .dialogs import ExportKontorDialog, ImportKontorDialog from .model_config import KontorModelConfig @@ -62,8 +63,11 @@ class MainWindow(QMainWindow): self.newAction = QAction("&New", self) self.aboutAction = QAction("&Über...", self) self.aboutAction.triggered.connect(self.about) - self.showComicWindow = QAction("Show/Hide &Comic Window", self) + self.showComicWindow = QAction("&Comic Window", self) self.showComicWindow.triggered.connect(self.show_comic_window) + self.showTyscWindow = QAction("TYSC Window", self) + self.showMediaWindow = QAction("&Media Window", self) + self.showMediaWindow.triggered.connect(self.show_media_window) self.importAction = QAction(self.import_icon, "&Import", self) self.importAction.triggered.connect(self.import_from_file) self.exportAction = QAction(self.export_icon, "&Export", self) @@ -101,6 +105,12 @@ class MainWindow(QMainWindow): kontor_menu.addMenu(comic_menu) kontor_menu.addMenu(tysc_menu) kontor_menu.addMenu(media_file_menu) + window_menu = QMenu("&Window") + layouts_menu = QMenu("&Layouts") + window_menu.addMenu(layouts_menu) + window_menu.addAction(self.showComicWindow) + window_menu.addAction(self.showMediaWindow) + menu_bar.addMenu(window_menu) # Help menu help_menu = QMenu("&Hilfe") menu_bar.addMenu(help_menu) @@ -135,6 +145,18 @@ class MainWindow(QMainWindow): comic.close() self.mdi_area.removeSubWindow(comic) + def show_media_window(self): + if 'media' not in self._subwindows: + media = MediaWindow(self) + media.closed.connect(self.sub_window_closed) + self._subwindows['media'] = media + self.mdi_area.addSubWindow(media) + media.show() + else: + media = self._subwindows.pop('media') + media.close() + self.mdi_area.removeSubWindow(media) + def remove_sub_window(self, name: str): # self.log.info("remove subwindow %s", name) if name in self._subwindows: diff --git a/python/kontor-gui/gui/media_window.py b/python/kontor-gui/gui/media_window.py new file mode 100644 index 0000000..6518953 --- /dev/null +++ b/python/kontor-gui/gui/media_window.py @@ -0,0 +1,60 @@ +from PySide6.QtCore import Signal +from PySide6.QtWidgets import QMdiSubWindow, QWidget, QVBoxLayout, QTabWidget, QTableView + +from gui.model_config import KontorModelConfig +from gui.table_model import KontorTableModel + + +class MediaWindow(QMdiSubWindow): + closed = Signal() + + def __init__(self, main_window): + super().__init__() + self.data_views = list() + self._main_window = main_window + self.log = main_window.log + self._init_gui() + self.tick = main_window.tick + self.cross = main_window.cross + + def _init_gui(self): + self.setWindowTitle("Media") + self.setWidget(QWidget()) + layout = QVBoxLayout() + self.tabs = QTabWidget() + self.tabs.addTab(self.generate_data_tab("media_file"), "Media File") + self.tabs.addTab(self.generate_data_tab("media_video"), "Media Video") + self.tabs.currentChanged.connect(self._tab_changed) + layout.addWidget(self.tabs) + self.setLayout(layout) + self.setWidget(self.tabs) + + def closeEvent(self, event): + self.closed.emit() + super().closeEvent(event) + self._main_window.remove_sub_window('comic') + + def refresh(self): + # self.log.info("refresh") + self.data_views[self.tabs.currentIndex()].refresh() + + def _tab_changed(self, tab_index): + self.data_views[tab_index].refresh() + + def update_status(self, message): + self._main_window.update_status(message) + + def generate_data_tab(self, table_name): + data_tab = QWidget() + + table_config = KontorModelConfig(self._main_window.kontor_db, self, table_name) + model = KontorTableModel(table_config) + layout = QVBoxLayout() + self.data_views.append(model) + data_tab.setLayout(layout) + table_view = QTableView() + table_view.setModel(model) + layout.addLayout(table_config.get_filter_layout()) + layout.addWidget(table_view) + model.refresh() + return data_tab From 1e9ca7c1a48d5cf8061b548f2299f0f892b7137f Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sat, 25 Jan 2025 03:53:47 +0100 Subject: [PATCH 66/91] add Enum for Videotype --- python/kontor-cli/kontor/controllers/media.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/python/kontor-cli/kontor/controllers/media.py b/python/kontor-cli/kontor/controllers/media.py index da4420c..0f5e610 100644 --- a/python/kontor-cli/kontor/controllers/media.py +++ b/python/kontor-cli/kontor/controllers/media.py @@ -1,3 +1,4 @@ +from enum import Enum from pathlib import Path from cement import Controller, ex @@ -5,6 +6,11 @@ from kontor_schema import KontorDB from kontor_video import VideoLink, MediaVideo +class VideoType(Enum): + MEDIA_FILE = "media_file" + MEDIA_VIDEO = "media_video" + + class Media(Controller): class Meta: label = 'media' @@ -45,7 +51,7 @@ class Media(Controller): downloads = db.get_download_list() self.app.log.info(f"found {len(downloads)} links for download") for file_id, url in downloads.items(): - link = VideoLink(url) + link = VideoLink(url, VideoType.MEDIA_FILE) file_name = link.download(download_dir=data['media_dir']) if file_name is None: db.update_entry('media_file', file_id, {'file_name': None, 'should_download': 1}) From fe89cc6e0f9614d60fb0290099885f7f3d0049c7 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 27 Jan 2025 13:34:15 +0100 Subject: [PATCH 67/91] add Mixin for videos --- python/kontor-schema/kontor_schema/base.py | 13 ++++++++++++- python/kontor-schema/kontor_schema/media.py | 11 ++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/python/kontor-schema/kontor_schema/base.py b/python/kontor-schema/kontor_schema/base.py index 21186d4..f1cac9f 100644 --- a/python/kontor-schema/kontor_schema/base.py +++ b/python/kontor-schema/kontor_schema/base.py @@ -1,7 +1,8 @@ import uuid from datetime import datetime -from sqlalchemy import func +from sqlalchemy import func, Column, String +from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column @@ -18,3 +19,13 @@ class BaseMixin: last_modified_date: Mapped[datetime] = mapped_column(default=func.now()) # version = Column(Integer) version: Mapped[int] = mapped_column(default=0) + +class BaseVideoMixin: + cloud_link = Column(String(255)) + file_name = Column(String(255)) + path = Column(String(255)) + review = Column(BIT(1)) + title = Column(String(255)) + url = Column(String(255), unique=True) + should_download = Column(BIT(1)) + diff --git a/python/kontor-schema/kontor_schema/media.py b/python/kontor-schema/kontor_schema/media.py index d9d1bf2..c259669 100644 --- a/python/kontor-schema/kontor_schema/media.py +++ b/python/kontor-schema/kontor_schema/media.py @@ -1,18 +1,11 @@ from sqlalchemy import Column, DateTime, Integer, String from sqlalchemy.dialects.mysql import BIT -from .base import Base, BaseMixin +from .base import Base, BaseMixin, BaseVideoMixin -class MediaFile(Base, BaseMixin): +class MediaFile(Base, BaseMixin, BaseVideoMixin): __tablename__ = 'media_file' - cloud_link = Column(String(255)) - file_name = Column(String(255)) - path = Column(String(255)) - review = Column(BIT(1)) - title = Column(String(255)) - url = Column(String(255), unique=True) - should_download = Column(BIT(1)) def __repr__(self): return f'MediaFile({self.id} {self.title} {self.title})' From 93c7498a83b475efe0a8abfccabc9039b8c2728d Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 27 Jan 2025 15:20:35 +0100 Subject: [PATCH 68/91] setting return value of adding link to db --- python/kontor-schema/kontor_schema/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/kontor-schema/kontor_schema/__init__.py b/python/kontor-schema/kontor_schema/__init__.py index bb1db87..74f672a 100644 --- a/python/kontor-schema/kontor_schema/__init__.py +++ b/python/kontor-schema/kontor_schema/__init__.py @@ -263,7 +263,7 @@ class KontorDB: return existing_ids def add_entry(self, table_name: str, update_item: dict): - # self.log.info("add entry to table %s with %s", table_name, update_item) + self.log.debug("add entry to table %s with %s", table_name, update_item) __session__ = sessionmaker(self.engine) with __session__() as session: add_item = self.registry[table_name]() @@ -307,7 +307,7 @@ class KontorDB: try: session.add(media_file) session.commit() - result['added'] = media_file + result['added'] = {'url': media_file.url, 'title': media_file.title, 'review': media_file.review, 'download': media_file.should_download} except IntegrityError as error: session.rollback() result['error'] = error.orig From d01489b1fa17581a699864a7b798c3505d074c1b Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 27 Jan 2025 15:21:51 +0100 Subject: [PATCH 69/91] reformat python file --- python/kontor-schema/kontor_schema/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/kontor-schema/kontor_schema/base.py b/python/kontor-schema/kontor_schema/base.py index f1cac9f..dd4dbad 100644 --- a/python/kontor-schema/kontor_schema/base.py +++ b/python/kontor-schema/kontor_schema/base.py @@ -20,6 +20,7 @@ class BaseMixin: # version = Column(Integer) version: Mapped[int] = mapped_column(default=0) + class BaseVideoMixin: cloud_link = Column(String(255)) file_name = Column(String(255)) @@ -28,4 +29,3 @@ class BaseVideoMixin: title = Column(String(255)) url = Column(String(255), unique=True) should_download = Column(BIT(1)) - From c61e49720e8feeb3227921d9d2d06ae06b67ccb9 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 27 Jan 2025 22:42:32 +0100 Subject: [PATCH 70/91] update title --- python/kontor-cli/kontor/controllers/media.py | 41 ++++++++----------- python/kontor-gui/requirements.txt | 3 ++ .../kontor-schema/kontor_schema/__init__.py | 23 ++++++----- python/kontor-schema/kontor_schema/media.py | 17 ++++++++ python/kontor-schema/requirements.txt | 2 + 5 files changed, 53 insertions(+), 33 deletions(-) diff --git a/python/kontor-cli/kontor/controllers/media.py b/python/kontor-cli/kontor/controllers/media.py index 0f5e610..135366e 100644 --- a/python/kontor-cli/kontor/controllers/media.py +++ b/python/kontor-cli/kontor/controllers/media.py @@ -25,11 +25,6 @@ class Media(Controller): db = self.app.kontor_db updates = db.get_update_list() self.app.log.info(f"found {len(updates)} links for update") - for file_id, url in updates.items(): - link = MediaVideo(url) - title = link.get_title() - if title is not None: - db.update_entry('media_file', file_id, {'title': title, 'review': 0,}) @ex( label='download', @@ -48,25 +43,25 @@ class Media(Controller): if self.app.pargs.media_dir is not None: data['media_dir'] = self.app.pargs.media_dir db = self.app.kontor_db - downloads = db.get_download_list() + downloads = db.get_download_list(data['media_dir']) self.app.log.info(f"found {len(downloads)} links for download") - for file_id, url in downloads.items(): - link = VideoLink(url, VideoType.MEDIA_FILE) - file_name = link.download(download_dir=data['media_dir']) - if file_name is None: - db.update_entry('media_file', file_id, {'file_name': None, 'should_download': 1}) - else: - download_file = Path(file_name) - download_file.with_name(f"{file_id}{download_file.suffix}") - link.file_name = download_file.name - link.should_download = 0 - link.cloud_link = download_file.absolute() - db.update_entry('media_file', file_id, - { - 'file_name': download_file.name, - 'should_download': 0, - 'cloud_link': download_file.absolute()} - ) + #for file_id, url in downloads.items(): + # link = VideoLink(url, VideoType.MEDIA_FILE) + # file_name = link.download(download_dir=data['media_dir']) + # if file_name is None: + # db.update_entry('media_file', file_id, {'file_name': None, 'should_download': 1}) + # else: + # download_file = Path(file_name) + # download_file.with_name(f"{file_id}{download_file.suffix}") + # link.file_name = download_file.name + # link.should_download = 0 + # link.cloud_link = download_file.absolute() + # db.update_entry('media_file', file_id, + # { + # 'file_name': download_file.name, + # 'should_download': 0, + # 'cloud_link': download_file.absolute()} + # ) @ex( help='add url to database', diff --git a/python/kontor-gui/requirements.txt b/python/kontor-gui/requirements.txt index 057d486..6c37bb1 100644 --- a/python/kontor-gui/requirements.txt +++ b/python/kontor-gui/requirements.txt @@ -4,3 +4,6 @@ platformdirs pyyaml PySide6 +beautifulsoup4 +requests + diff --git a/python/kontor-schema/kontor_schema/__init__.py b/python/kontor-schema/kontor_schema/__init__.py index 74f672a..f14b16a 100644 --- a/python/kontor-schema/kontor_schema/__init__.py +++ b/python/kontor-schema/kontor_schema/__init__.py @@ -199,7 +199,7 @@ class KontorDB: self.delete_entries() import_file = Path(import_file_name) if not import_file.exists(): - self.log.info("File %s does not exist. Do nothing.", import_file_name) + self.log.info(f"File {import_file_name} does not exist. Do nothing.") return result match import_file.suffix: case '.json': @@ -207,7 +207,7 @@ class KontorDB: with open(import_file_name, 'r') as json_file: json_load = json.load(json_file) for table in json_load: - self.log.info("%s: %d", table, len(json_load[table])) + self.log.info(f"{table}: {len(json_load[table])}") result[table] = self.import_table(table, json_load[table]) case '.yml': print("read yaml file") @@ -223,7 +223,7 @@ class KontorDB: added = [] remaining = [] existing_ids = self.get_ids(table_name) - self.log.info("found %d existing ids for table %s", len(existing_ids), table_name) + self.log.info(f"found {len(existing_ids)} existing ids for table {table_name}") for item in items: current_id = item['id'] # print(f"import item: {item}") @@ -236,7 +236,7 @@ class KontorDB: changed = self.update_entry(table_name, current_id, item) updated.append(item) if changed: - self.log.info("%s has changed", current_id) + self.log.info(f"{current_id} has changed") updated.append(item) existing_ids.remove(current_id) else: @@ -244,7 +244,7 @@ class KontorDB: self.add_entry(table_name, item) added.append(item) except IntegrityError as error: - self.log.info("Could not add item, due to: %s", error.detail) + self.log.info(f"Could not add item, due to: {error.detail}") if len(existing_ids) > 0: print(f"remaining items: {existing_ids}") remaining.extend(existing_ids) @@ -263,7 +263,7 @@ class KontorDB: return existing_ids def add_entry(self, table_name: str, update_item: dict): - self.log.debug("add entry to table %s with %s", table_name, update_item) + self.log.debug(f"add entry to table {table_name} with {update_item}") __session__ = sessionmaker(self.engine) with __session__() as session: add_item = self.registry[table_name]() @@ -289,7 +289,7 @@ class KontorDB: setattr(existing_item, key, update_value) session.commit() changed = True - self.log.info("update {key} with {update_value}", (key, update_value)) + self.log.info(f"update {key} with {update_value}") return changed def add_link(self, link: str) -> dict: @@ -322,10 +322,12 @@ class KontorDB: url = link.url if url is None: continue - update_list[link.id] = url + link.update_title() + session.commit() + update_list[link.id] = link.title return update_list - def get_download_list(self) -> dict: + def get_download_list(self, download_dir: str) -> dict: download_list = {} __session__ = sessionmaker(self.engine) with __session__() as session: @@ -334,7 +336,8 @@ class KontorDB: url = link.url if url is None: continue - download_list[link.id] = url + link.download_file(download_dir) + download_list[link.id] = link.file_name return download_list def delete_entries(self): diff --git a/python/kontor-schema/kontor_schema/media.py b/python/kontor-schema/kontor_schema/media.py index c259669..ab1e8b4 100644 --- a/python/kontor-schema/kontor_schema/media.py +++ b/python/kontor-schema/kontor_schema/media.py @@ -1,3 +1,5 @@ +import requests +from bs4 import BeautifulSoup from sqlalchemy import Column, DateTime, Integer, String from sqlalchemy.dialects.mysql import BIT @@ -13,6 +15,21 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin): def __str__(self): return f'{self.title}({self.id})' + def update_title(self): + print(f"update title for {self.url}") + try: + r = requests.get(self.url) + soup = BeautifulSoup(r.content, "html.parser") + title = soup.title.string + self.title = title + self.review = 0 + except: + self.title = None + self.review = 1 + + def download_file(self, download_dir: str): + print(f"download file for {self.url}") + class MediaArticle(Base, BaseMixin): __tablename__ = 'media_article' diff --git a/python/kontor-schema/requirements.txt b/python/kontor-schema/requirements.txt index d08ec81..4f0e48d 100644 --- a/python/kontor-schema/requirements.txt +++ b/python/kontor-schema/requirements.txt @@ -1,2 +1,4 @@ mariadb sqlalchemy +beautifulsoup4 +requests From e733fa21e60fc9748b8354aaed13c810e3eaf067 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Tue, 28 Jan 2025 15:10:10 +0100 Subject: [PATCH 71/91] moved update and download functionality to kontor-schema --- python/kontor-cli/kontor/controllers/media.py | 37 +++-------- python/kontor-cli/requirements.txt | 1 - python/kontor-gui/gui/main_window.py | 52 ++++++++------- python/kontor-gui/gui/media_window.py | 2 +- python/kontor-gui/gui/worker.py | 28 +++++++++ python/kontor-gui/requirements.txt | 3 - .../kontor-schema/kontor_schema/__init__.py | 18 ++++-- python/kontor-schema/kontor_schema/media.py | 39 +++++++++++- python/kontor-schema/setup.py | 4 +- python/kontor-video/README.md | 3 - python/kontor-video/kontor_video/__init__.py | 63 ------------------- python/kontor-video/pyvenv.cfg | 5 -- python/kontor-video/requirements.txt | 2 - python/kontor-video/setup.py | 23 ------- 14 files changed, 118 insertions(+), 162 deletions(-) create mode 100644 python/kontor-gui/gui/worker.py delete mode 100644 python/kontor-video/README.md delete mode 100644 python/kontor-video/kontor_video/__init__.py delete mode 100644 python/kontor-video/pyvenv.cfg delete mode 100644 python/kontor-video/requirements.txt delete mode 100644 python/kontor-video/setup.py diff --git a/python/kontor-cli/kontor/controllers/media.py b/python/kontor-cli/kontor/controllers/media.py index 135366e..55148fc 100644 --- a/python/kontor-cli/kontor/controllers/media.py +++ b/python/kontor-cli/kontor/controllers/media.py @@ -1,14 +1,4 @@ -from enum import Enum -from pathlib import Path - from cement import Controller, ex -from kontor_schema import KontorDB -from kontor_video import VideoLink, MediaVideo - - -class VideoType(Enum): - MEDIA_FILE = "media_file" - MEDIA_VIDEO = "media_video" class Media(Controller): @@ -23,8 +13,8 @@ class Media(Controller): ) def update_title(self): db = self.app.kontor_db - updates = db.get_update_list() - self.app.log.info(f"found {len(updates)} links for update") + updates = db.update_titles() + self.app.log.info(f"{len(updates)} entries updated") @ex( label='download', @@ -43,25 +33,12 @@ class Media(Controller): if self.app.pargs.media_dir is not None: data['media_dir'] = self.app.pargs.media_dir db = self.app.kontor_db - downloads = db.get_download_list(data['media_dir']) + downloads = db.get_download_list() self.app.log.info(f"found {len(downloads)} links for download") - #for file_id, url in downloads.items(): - # link = VideoLink(url, VideoType.MEDIA_FILE) - # file_name = link.download(download_dir=data['media_dir']) - # if file_name is None: - # db.update_entry('media_file', file_id, {'file_name': None, 'should_download': 1}) - # else: - # download_file = Path(file_name) - # download_file.with_name(f"{file_id}{download_file.suffix}") - # link.file_name = download_file.name - # link.should_download = 0 - # link.cloud_link = download_file.absolute() - # db.update_entry('media_file', file_id, - # { - # 'file_name': download_file.name, - # 'should_download': 0, - # 'cloud_link': download_file.absolute()} - # ) + for entry_id in downloads: + result = db.download_file(entry_id, download_dir=data['media_dir']) + if result is not None: + self.app.log.info(f"file {result} successfully downloaded") @ex( help='add url to database', diff --git a/python/kontor-cli/requirements.txt b/python/kontor-cli/requirements.txt index 58833ba..814fe5a 100644 --- a/python/kontor-cli/requirements.txt +++ b/python/kontor-cli/requirements.txt @@ -1,5 +1,4 @@ -e ../kontor-schema --e ../kontor-video cement==3.0.12 cement[jinja2] diff --git a/python/kontor-gui/gui/main_window.py b/python/kontor-gui/gui/main_window.py index 5a97c6a..6ddd612 100644 --- a/python/kontor-gui/gui/main_window.py +++ b/python/kontor-gui/gui/main_window.py @@ -1,3 +1,4 @@ +from PySide6.QtCore import Qt, QThreadPool from PySide6.QtGui import QAction, QIcon, QGuiApplication from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView, QProgressBar, QMdiArea from PySide6.QtWidgets import QLabel, QMainWindow @@ -10,6 +11,7 @@ from .progress import ProgressUpdate from .dialogs import ExportKontorDialog, ImportKontorDialog from .model_config import KontorModelConfig from .table_model import KontorTableModel +from .worker import VideoDownloader class MainWindow(QMainWindow): @@ -17,6 +19,7 @@ class MainWindow(QMainWindow): def __init__(self, engine: Engine, log): super().__init__() + self.downloader = None self.tick = QIcon('res/tick.png') self.cross = QIcon('res/cross.png') self.import_icon = QIcon("res/application-import.png") @@ -28,29 +31,20 @@ class MainWindow(QMainWindow): self.kontor_db = KontorDB(engine, log) self.log = log self._subwindows = {} + self.media_dir = "/data/media" + self.dl_tool = "yt-dlp" self._setup_ui() - - #self.tabs = QTabWidget() - #self.tabs.addTab(self.generate_data_tab("comic"), "Comics") - #self.tabs.addTab(self.generate_data_tab("media_file"), "MediaFile") - #self.tabs.currentChanged.connect(self._tab_changed) - #label.setAlignment(Qt.AlignmentFlag.AlignCenter) - #parent_layout.addWidget(self.tabs) - - self.setCentralWidget(self.central_widget) - def _setup_ui(self): self.setWindowTitle("Kontor") self.setMinimumSize(1200, 800) self._create_actions() - self.central_widget = QWidget() - # parent_layout = QVBoxLayout() - # self.central_widget.setLayout(parent_layout) - self.mdi_area = QMdiArea(self.central_widget) + self.mdi_area = QMdiArea() + self.setCentralWidget(self.mdi_area) self.mdi_area.setObjectName('mdi_area') - self.setCentralWidget(self.central_widget) + self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) self._create_menubar() self._create_toolbars() self.status_progress = QProgressBar() @@ -77,7 +71,7 @@ class MainWindow(QMainWindow): self.updateTitleAction = QAction("&Update Titles", self) self.updateTitleAction.triggered.connect(self.update_title) self.downloadAction = QAction("&Download Videos", self) - self.downloadAction.triggered.connect(self.download_file) + self.downloadAction.triggered.connect(self.start_download) self.checkFileAction = QAction("&Check files", self) self.checkFileAction.triggered.connect(self.check_files) self.exitAction = QAction("&Beenden", self) @@ -189,18 +183,32 @@ class MainWindow(QMainWindow): self.log.info("update title for table MediaFile") self.statusBar.showMessage("update title for table MediaFile", 3000) self.status_progress.setEnabled(True) - self.kontor_db.update_title() + self.kontor_db.update_titles() self.status_progress.setEnabled(False) self.refresh() - def download_file(self): - self.log.info("download videos for table MediaFile") - self.statusBar.showMessage("download videos for table MediaFile", 3000) + def start_download(self): self.status_progress.setEnabled(True) - self.kontor_db.download_file(False, self.progress_update) - self.status_progress.setEnabled(False) + self.statusBar.showMessage("download videos for table MediaFile", 3000) + self.downloader = VideoDownloader(self.kontor_db, self.log) + self.downloader.setTotalProgress.connect(self.status_progress.setMaximum) + self.downloader.setCurrentProgress.connect(self.downloadProgress) + self.downloader.succeeded.connect(self.downloadSucceeded) + self.downloader.finished.connect(self.downloadFinished) + self.downloader.start() + + def downloadProgress(self, value: int): + self.status_progress.setValue(value) self.refresh() + def downloadSucceeded(self): + self.status_progress.setValue(self.status_progress.maximum()) + self.statusBar.showMessage("Download succeeded", 3000) + + def downloadFinished(self): + self.status_progress.setEnabled(False) + del self.downloader + def check_files(self): self.log.info("check files") self.statusBar.showMessage("check files for table MediaFile", 3000) diff --git a/python/kontor-gui/gui/media_window.py b/python/kontor-gui/gui/media_window.py index 6518953..44c30a8 100644 --- a/python/kontor-gui/gui/media_window.py +++ b/python/kontor-gui/gui/media_window.py @@ -35,7 +35,7 @@ class MediaWindow(QMdiSubWindow): self._main_window.remove_sub_window('comic') def refresh(self): - # self.log.info("refresh") + self.log.info("MediaWindow.refresh") self.data_views[self.tabs.currentIndex()].refresh() def _tab_changed(self, tab_index): diff --git a/python/kontor-gui/gui/worker.py b/python/kontor-gui/gui/worker.py new file mode 100644 index 0000000..96ecf12 --- /dev/null +++ b/python/kontor-gui/gui/worker.py @@ -0,0 +1,28 @@ +import sys + +from PySide6.QtCore import QObject, Signal, QRunnable, Slot, QThread + + +class VideoDownloader(QThread): + # Signal for the window to establish the maximum value + # of the progress bar. + setTotalProgress = Signal(int) + # Signal to increase the progress. + setCurrentProgress = Signal(int) + # Signal to be emitted when the file has been downloaded successfully. + succeeded = Signal() + + def __init__(self, kontor_db, log): + super().__init__() + self.kontor_db = kontor_db + self.log = log + + def run(self): + self.log.info("download videos for table MediaFile") + download_entries = self.kontor_db.get_download_list() + self.setTotalProgress.emit(len(download_entries)) + for index, entry in enumerate(download_entries): + self.kontor_db.download_file(entry) + self.setCurrentProgress.emit(index) + self.succeeded.emit() + diff --git a/python/kontor-gui/requirements.txt b/python/kontor-gui/requirements.txt index 6c37bb1..a6e9a93 100644 --- a/python/kontor-gui/requirements.txt +++ b/python/kontor-gui/requirements.txt @@ -1,9 +1,6 @@ -e ../kontor-schema --e ../kontor-video platformdirs pyyaml PySide6 -beautifulsoup4 -requests diff --git a/python/kontor-schema/kontor_schema/__init__.py b/python/kontor-schema/kontor_schema/__init__.py index f14b16a..1e747a1 100644 --- a/python/kontor-schema/kontor_schema/__init__.py +++ b/python/kontor-schema/kontor_schema/__init__.py @@ -313,7 +313,7 @@ class KontorDB: result['error'] = error.orig return result - def get_update_list(self) -> dict: + def update_titles(self) -> dict: update_list = {} __session__ = sessionmaker(self.engine) with __session__() as session: @@ -327,8 +327,8 @@ class KontorDB: update_list[link.id] = link.title return update_list - def get_download_list(self, download_dir: str) -> dict: - download_list = {} + def get_download_list(self) -> list: + download_list = [] __session__ = sessionmaker(self.engine) with __session__() as session: links = session.query(MediaFile).filter(MediaFile.should_download == 1).all() @@ -336,10 +336,18 @@ class KontorDB: url = link.url if url is None: continue - link.download_file(download_dir) - download_list[link.id] = link.file_name + download_list.append(link.id) return download_list + def download_file(self, entry_id: str, download_dir = "/data/media", dl_tool = "yt-dlp") -> str: + __session__ = sessionmaker(self.engine) + with __session__() as session: + link = session.query(MediaFile).get(entry_id) + link.download_file(download_dir, dl_tool) + session.commit() + file_name = link.file_name + return file_name + def delete_entries(self): for (table_name, table) in self.registry.items(): # self.log.info("delete entries from table %s", table_name) diff --git a/python/kontor-schema/kontor_schema/media.py b/python/kontor-schema/kontor_schema/media.py index ab1e8b4..10338b4 100644 --- a/python/kontor-schema/kontor_schema/media.py +++ b/python/kontor-schema/kontor_schema/media.py @@ -1,3 +1,8 @@ +import re +import subprocess +from datetime import datetime +from pathlib import Path + import requests from bs4 import BeautifulSoup from sqlalchemy import Column, DateTime, Integer, String @@ -26,9 +31,39 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin): except: self.title = None self.review = 1 + self.last_modified_date = datetime.now() - def download_file(self, download_dir: str): - print(f"download file for {self.url}") + def download_file(self, download_dir: str, dl_tool: str): + print(f"download file for {self.url} to {download_dir}") + result = subprocess.run([dl_tool, self.url], cwd=download_dir, capture_output=True, text=True) + if result.returncode == 0: + output = result.stdout + output = re.sub(' +', ' ', output) + lines_list = output.splitlines() + file_name = self.__parse_output__(lines_list) + if file_name is None: + self.review = 1 + self.should_download = 1 + self.file_name = None + else: + download_file = Path(file_name) + self.should_download = 0 + self.file_name = download_file.name + self.cloud_link = str(download_file.absolute()) + self.last_modified_date = datetime.now() + + def __parse_output__(self, lines_list): + self.file_name = None + for line in lines_list: + if 'has already been downloaded' in line: + end_len = len(' has already been downloaded') + self.file_name = line[11:-end_len] + if 'Destination' in line: + line_len = len(line) + start_len = len('[download] Destination: ') + file_len = line_len - start_len + self.file_name = line[-file_len:] + return self.file_name class MediaArticle(Base, BaseMixin): diff --git a/python/kontor-schema/setup.py b/python/kontor-schema/setup.py index 7f76710..99f68b9 100644 --- a/python/kontor-schema/setup.py +++ b/python/kontor-schema/setup.py @@ -8,7 +8,7 @@ long_description = ( here / "README.md").read_text(encoding="utf-8") setup( name='kontor_schema', version='0.1.0', - description='Schema for Konotor DB', + description='Schema for Kontor DB', long_description=long_description, long_description_content_type="text/markdown", author='Thomas Peetz', @@ -18,6 +18,6 @@ setup( "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.11", ], - install_requires=["sqlalchemy", "mariadb"], + install_requires=["sqlalchemy", "mariadb", "requests", "beautifulsoup4"], packages=find_packages(), ) diff --git a/python/kontor-video/README.md b/python/kontor-video/README.md deleted file mode 100644 index c52965f..0000000 --- a/python/kontor-video/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Kontor Video - -This project provides helper methods to handle video links, like Youtube or ZDF Mediathek. diff --git a/python/kontor-video/kontor_video/__init__.py b/python/kontor-video/kontor_video/__init__.py deleted file mode 100644 index 8cfb548..0000000 --- a/python/kontor-video/kontor_video/__init__.py +++ /dev/null @@ -1,63 +0,0 @@ -import re -import subprocess -from pathlib import Path - -import requests -from bs4 import BeautifulSoup - - -class VideoLink: - - def __init__(self, url: str, dl_tool: str, table: str): - self.file_name = None - self.url = url - self.title = None - self.dl_tool = dl_tool - self.table = table - - def get_title(self) -> str: - try: - r = requests.get(self.url) - soup = BeautifulSoup(r.content, "html.parser") - title = soup.title.string - except: - title = None - return title - - - def download(self, download_dir=None): - if download_dir is None: - download_dir = Path.cwd() - result = subprocess.run([self.dl_tool, self.url], cwd=download_dir, capture_output=True, text=True) - if result.returncode == 0: - output = result.stdout - output = re.sub(' +', ' ', output) - lines_list = output.splitlines() - return self.__parse_output__(lines_list) - else: - return None - - def __parse_output__(self, lines_list): - self.file_name = "" - for line in lines_list: - if 'has already been downloaded' in line: - end_len = len(' has already been downloaded') - self.file_name = line[11:-end_len] - if 'Destination' in line: - line_len = len(line) - start_len = len('[download] Destination: ') - file_len = line_len - start_len - self.file_name = line[-file_len:] - return self.file_name - - -class MediaFile(VideoLink): - - def __init__(self, url: str, dl_tool='yt-dlp'): - super().__init__(url, dl_tool, 'media_file') - - -class MediaVideo(VideoLink): - - def __init__(self, url: str, dl_tool='yt-dlp'): - super().__init__(url, dl_tool, 'media_video') diff --git a/python/kontor-video/pyvenv.cfg b/python/kontor-video/pyvenv.cfg deleted file mode 100644 index e789070..0000000 --- a/python/kontor-video/pyvenv.cfg +++ /dev/null @@ -1,5 +0,0 @@ -home = /usr/bin -include-system-site-packages = false -version = 3.11.2 -executable = /usr/bin/python3.11 -command = /usr/bin/python -m venv /home/tpeetz/projects/kontor/python/kontor-video diff --git a/python/kontor-video/requirements.txt b/python/kontor-video/requirements.txt deleted file mode 100644 index 1f3e778..0000000 --- a/python/kontor-video/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -beautifulsoup4 -requests diff --git a/python/kontor-video/setup.py b/python/kontor-video/setup.py deleted file mode 100644 index 1362fdf..0000000 --- a/python/kontor-video/setup.py +++ /dev/null @@ -1,23 +0,0 @@ -from setuptools import setup, find_packages -import pathlib - -here = pathlib.Path(__file__).parent.resolve() - -long_description = ( here / "README.md").read_text(encoding="utf-8") - -setup( - name='kontor_video', - version='0.1.0', - description='Helper methods to download videos', - long_description=long_description, - long_description_content_type="text/markdown", - author='Thomas Peetz', - classifiers=[ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.11", - ], - install_requires=["beautifulsoup4"], - packages=find_packages(), -) From da453d642df04d67a33161155147f62847a4c425 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Tue, 28 Jan 2025 23:19:23 +0100 Subject: [PATCH 72/91] rename kontor.py to main.py for pyside6-deploy --- python/kontor-gui/{kontor.py => main.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename python/kontor-gui/{kontor.py => main.py} (100%) diff --git a/python/kontor-gui/kontor.py b/python/kontor-gui/main.py similarity index 100% rename from python/kontor-gui/kontor.py rename to python/kontor-gui/main.py From 591171b22396bb074ef57ddd55e640a445020a9f Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 2 Feb 2025 21:38:16 +0100 Subject: [PATCH 73/91] add meta data subwindow --- python/kontor-gui/gui/comic_window.py | 9 ++- python/kontor-gui/gui/main_window.py | 43 ++++++------ python/kontor-gui/gui/media_window.py | 9 ++- python/kontor-gui/gui/meta_data_window.py | 65 +++++++++++++++++++ .../kontor-schema/kontor_schema/__init__.py | 6 +- 5 files changed, 99 insertions(+), 33 deletions(-) create mode 100644 python/kontor-gui/gui/meta_data_window.py diff --git a/python/kontor-gui/gui/comic_window.py b/python/kontor-gui/gui/comic_window.py index b9f9b9f..5f465d6 100644 --- a/python/kontor-gui/gui/comic_window.py +++ b/python/kontor-gui/gui/comic_window.py @@ -1,4 +1,4 @@ -from PySide6.QtCore import Signal +from PySide6.QtCore import Signal, QSortFilterProxyModel from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QTabWidget, QMenu, QTableView, QMdiSubWindow from gui.model_config import KontorModelConfig @@ -53,9 +53,12 @@ class ComicWindow(QMdiSubWindow): self.data_views.append(model) data_tab.setLayout(layout) table_view = QTableView() - table_view.setModel(model) + proxy_model = QSortFilterProxyModel() + proxy_model.setSourceModel(model) + table_view.setSortingEnabled(True) + #table_view.setModel(model) + table_view.setModel(proxy_model) layout.addLayout(table_config.get_filter_layout()) layout.addWidget(table_view) model.refresh() return data_tab - diff --git a/python/kontor-gui/gui/main_window.py b/python/kontor-gui/gui/main_window.py index 6ddd612..6a2b591 100644 --- a/python/kontor-gui/gui/main_window.py +++ b/python/kontor-gui/gui/main_window.py @@ -7,6 +7,7 @@ from kontor_schema import KontorDB from .comic_window import ComicWindow from .media_window import MediaWindow +from .meta_data_window import MetaDataWindow from .progress import ProgressUpdate from .dialogs import ExportKontorDialog, ImportKontorDialog from .model_config import KontorModelConfig @@ -50,8 +51,8 @@ class MainWindow(QMainWindow): self.status_progress = QProgressBar() self.progress_update = ProgressUpdate(self.status_progress) self._create_statusbar() - centerPoint = QGuiApplication.screens()[0].geometry().center() - self.move(centerPoint - self.frameGeometry().center()) + center_point = QGuiApplication.screens()[0].geometry().center() + self.move(center_point - self.frameGeometry().center()) def _create_actions(self): self.newAction = QAction("&New", self) @@ -62,6 +63,8 @@ class MainWindow(QMainWindow): self.showTyscWindow = QAction("TYSC Window", self) self.showMediaWindow = QAction("&Media Window", self) self.showMediaWindow.triggered.connect(self.show_media_window) + self.showMetaDataWindow = QAction("Meta Data Window", self) + self.showMetaDataWindow.triggered.connect(self.show_meta_data_window) self.importAction = QAction(self.import_icon, "&Import", self) self.importAction.triggered.connect(self.import_from_file) self.exportAction = QAction(self.export_icon, "&Export", self) @@ -104,6 +107,7 @@ class MainWindow(QMainWindow): window_menu.addMenu(layouts_menu) window_menu.addAction(self.showComicWindow) window_menu.addAction(self.showMediaWindow) + window_menu.addAction(self.showMetaDataWindow) menu_bar.addMenu(window_menu) # Help menu help_menu = QMenu("&Hilfe") @@ -125,7 +129,7 @@ class MainWindow(QMainWindow): self.statusBar.addPermanentWidget(self.status_progress) def about(self): - QMessageBox.about(self.central_widget, "Über Kontor", f"Python: 3.11\nKontor: 0.1.0") + QMessageBox.about(self, "Über Kontor", f"Python: 3.11\nKontor: 0.1.0") def show_comic_window(self): if 'comic' not in self._subwindows: @@ -151,6 +155,18 @@ class MainWindow(QMainWindow): media.close() self.mdi_area.removeSubWindow(media) + def show_meta_data_window(self): + if 'meta_data' not in self._subwindows: + meta_data = MetaDataWindow(self) + meta_data.closed.connect(self.sub_window_closed) + self._subwindows['meta_data'] = meta_data + self.mdi_area.addSubWindow(meta_data) + meta_data.show() + else: + meta_data = self._subwindows.pop('meta_data') + meta_data.close() + self.mdi_area.removeSubWindow(meta_data) + def remove_sub_window(self, name: str): # self.log.info("remove subwindow %s", name) if name in self._subwindows: @@ -218,27 +234,6 @@ class MainWindow(QMainWindow): self.log.info("refresh") for (_, window) in self._subwindows.items(): window.refresh() - # def refresh(self): - # self.data[self.tabs.currentIndex()].refresh() - - # def _tab_changed(self, tab_index): - # self.data[tab_index].refresh() def update_status(self, message, timeout=3000): self.statusBar.showMessage(message, timeout=timeout) - - def generate_data_tab(self, table_name): - data_tab = QWidget() - - table_config = KontorModelConfig(self.kontor_db, self, table_name) - model = KontorTableModel(table_config) - layout = QVBoxLayout() - self.data.append(model) - data_tab.setLayout(layout) - table_view = QTableView() - table_view.setModel(model) - layout.addLayout(table_config.get_filter_layout()) - layout.addWidget(table_view) - model.refresh() - return data_tab - diff --git a/python/kontor-gui/gui/media_window.py b/python/kontor-gui/gui/media_window.py index 44c30a8..175dd07 100644 --- a/python/kontor-gui/gui/media_window.py +++ b/python/kontor-gui/gui/media_window.py @@ -1,4 +1,4 @@ -from PySide6.QtCore import Signal +from PySide6.QtCore import Signal, QSortFilterProxyModel from PySide6.QtWidgets import QMdiSubWindow, QWidget, QVBoxLayout, QTabWidget, QTableView from gui.model_config import KontorModelConfig @@ -24,6 +24,7 @@ class MediaWindow(QMdiSubWindow): self.tabs = QTabWidget() self.tabs.addTab(self.generate_data_tab("media_file"), "Media File") self.tabs.addTab(self.generate_data_tab("media_video"), "Media Video") + self.tabs.addTab(self.generate_data_tab("media_article"), "Media Article") self.tabs.currentChanged.connect(self._tab_changed) layout.addWidget(self.tabs) self.setLayout(layout) @@ -53,7 +54,11 @@ class MediaWindow(QMdiSubWindow): self.data_views.append(model) data_tab.setLayout(layout) table_view = QTableView() - table_view.setModel(model) + proxy_model = QSortFilterProxyModel() + proxy_model.setSourceModel(model) + table_view.setSortingEnabled(True) + #table_view.setModel(model) + table_view.setModel(proxy_model) layout.addLayout(table_config.get_filter_layout()) layout.addWidget(table_view) model.refresh() diff --git a/python/kontor-gui/gui/meta_data_window.py b/python/kontor-gui/gui/meta_data_window.py new file mode 100644 index 0000000..77ce516 --- /dev/null +++ b/python/kontor-gui/gui/meta_data_window.py @@ -0,0 +1,65 @@ +from PySide6.QtCore import Signal, QSortFilterProxyModel +from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QTabWidget, QMenu, QTableView, QMdiSubWindow + +from gui.model_config import KontorModelConfig +from gui.table_model import KontorTableModel + + +class MetaDataWindow(QMdiSubWindow): + closed = Signal() + + def __init__(self, main_window): + super().__init__() + self.data_views = list() + self._main_window = main_window + self.log = main_window.log + self._init_gui() + self.tick = main_window.tick + self.cross = main_window.cross + + def _init_gui(self): + self.setWindowTitle("Meta Data") + self.setWidget(QWidget()) + layout = QVBoxLayout() + self.tabs = QTabWidget() + self.tabs.addTab(self.generate_data_tab("module_data"), "Module") + self.tabs.addTab(self.generate_data_tab("meta_data_table"), "Tables") + self.tabs.addTab(self.generate_data_tab("meta_data_column"), "Columns") + self.tabs.currentChanged.connect(self._tab_changed) + layout.addWidget(self.tabs) + self.setLayout(layout) + self.setWidget(self.tabs) + + def closeEvent(self, event): + self.closed.emit() + super().closeEvent(event) + self._main_window.remove_sub_window('comic') + + def refresh(self): + # self.log.info("refresh") + self.data_views[self.tabs.currentIndex()].refresh() + + def _tab_changed(self, tab_index): + self.data_views[tab_index].refresh() + + def update_status(self, message): + self._main_window.update_status(message) + + def generate_data_tab(self, table_name): + data_tab = QWidget() + + table_config = KontorModelConfig(self._main_window.kontor_db, self, table_name) + model = KontorTableModel(table_config) + layout = QVBoxLayout() + self.data_views.append(model) + data_tab.setLayout(layout) + table_view = QTableView() + proxy_model = QSortFilterProxyModel() + proxy_model.setSourceModel(model) + table_view.setSortingEnabled(True) + #table_view.setModel(model) + table_view.setModel(proxy_model) + layout.addLayout(table_config.get_filter_layout()) + layout.addWidget(table_view) + model.refresh() + return data_tab diff --git a/python/kontor-schema/kontor_schema/__init__.py b/python/kontor-schema/kontor_schema/__init__.py index 1e747a1..5cb8bb6 100644 --- a/python/kontor-schema/kontor_schema/__init__.py +++ b/python/kontor-schema/kontor_schema/__init__.py @@ -1,12 +1,9 @@ import json -import re -import subprocess import uuid from datetime import datetime from logging import Logger from pathlib import Path -import mariadb from sqlalchemy import Engine, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import sessionmaker @@ -137,10 +134,11 @@ class KontorDB: row = [] for order in columns.keys(): column_name = columns[order]['column'] + ref_column = columns[order]['ref_column'] if str(column_name).endswith("_id"): ref_table = column_name[:-3] ref = getattr(entry, ref_table) - value = getattr(ref, "name") + value = getattr(ref, ref_column) row.append(value) else: row.append(getattr(entry, column_name)) From f33aaadce79f2d141fa8a91c465e91b65cdab9f2 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 3 Feb 2025 17:37:19 +0100 Subject: [PATCH 74/91] fix problem with closing subwindows and define field length for id in schema --- python/kontor-gui/gui/main_window.py | 4 ++-- python/kontor-gui/gui/media_window.py | 2 +- python/kontor-gui/gui/meta_data_window.py | 2 +- python/kontor-schema/kontor_schema/admin.py | 2 +- python/kontor-schema/kontor_schema/base.py | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/python/kontor-gui/gui/main_window.py b/python/kontor-gui/gui/main_window.py index 6a2b591..a1921d7 100644 --- a/python/kontor-gui/gui/main_window.py +++ b/python/kontor-gui/gui/main_window.py @@ -168,7 +168,7 @@ class MainWindow(QMainWindow): self.mdi_area.removeSubWindow(meta_data) def remove_sub_window(self, name: str): - # self.log.info("remove subwindow %s", name) + self.log.info("remove subwindow %s", name) if name in self._subwindows: window = self._subwindows.pop(name) window.close() @@ -181,9 +181,9 @@ class MainWindow(QMainWindow): import_dlg = ImportKontorDialog(self) if import_dlg.exec(): print(f"import DB from file {import_dlg.file_name}") + self.kontor_db.import_db(import_dlg.file_name, False) else: print("do nothing for import") - pass def export_to_file(self): export_dlg = ExportKontorDialog(self, self.kontor_db) diff --git a/python/kontor-gui/gui/media_window.py b/python/kontor-gui/gui/media_window.py index 175dd07..1d9455f 100644 --- a/python/kontor-gui/gui/media_window.py +++ b/python/kontor-gui/gui/media_window.py @@ -33,7 +33,7 @@ class MediaWindow(QMdiSubWindow): def closeEvent(self, event): self.closed.emit() super().closeEvent(event) - self._main_window.remove_sub_window('comic') + self._main_window.remove_sub_window('media') def refresh(self): self.log.info("MediaWindow.refresh") diff --git a/python/kontor-gui/gui/meta_data_window.py b/python/kontor-gui/gui/meta_data_window.py index 77ce516..793fa16 100644 --- a/python/kontor-gui/gui/meta_data_window.py +++ b/python/kontor-gui/gui/meta_data_window.py @@ -33,7 +33,7 @@ class MetaDataWindow(QMdiSubWindow): def closeEvent(self, event): self.closed.emit() super().closeEvent(event) - self._main_window.remove_sub_window('comic') + self._main_window.remove_sub_window('meta_data') def refresh(self): # self.log.info("refresh") diff --git a/python/kontor-schema/kontor_schema/admin.py b/python/kontor-schema/kontor_schema/admin.py index b846d97..8dd33bd 100644 --- a/python/kontor-schema/kontor_schema/admin.py +++ b/python/kontor-schema/kontor_schema/admin.py @@ -35,7 +35,7 @@ class Token(Base, BaseMixin): name = Column(String(255)) last_used_date: Mapped[datetime] = mapped_column() enabled = Column(BIT(1)) - user_id = Column(String, ForeignKey("user.id"), nullable=False) + user_id = Column(String(255), ForeignKey("user.id"), nullable=False) user = relationship("User", back_populates="tokens") diff --git a/python/kontor-schema/kontor_schema/base.py b/python/kontor-schema/kontor_schema/base.py index dd4dbad..4a354e7 100644 --- a/python/kontor-schema/kontor_schema/base.py +++ b/python/kontor-schema/kontor_schema/base.py @@ -11,8 +11,8 @@ class Base(DeclarativeBase): class BaseMixin: - # id = Column(String, primary_key=True) - id: Mapped[str] = mapped_column(primary_key=True, default=uuid.uuid4()) + id = Column(String(255), primary_key=True, default=uuid.uuid4()) + # id: Mapped[str] = mapped_column(primary_key=True, default=uuid.uuid4()) # created_date = Column(DateTime) created_date: Mapped[datetime] = mapped_column(default=func.now()) # last_modified_date = Column(DateTime) From 71ecfaff1fb27d59a460ca263199e6537565c5fa Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 3 Feb 2025 23:28:35 +0100 Subject: [PATCH 75/91] make tables sortable --- python/kontor-gui/gui/comic_window.py | 5 ++- python/kontor-gui/gui/media_window.py | 4 +- python/kontor-gui/gui/meta_data_window.py | 14 +++--- python/kontor-gui/gui/model_config.py | 8 ++-- python/kontor-gui/gui/table_model.py | 41 ++++++++++-------- .../kontor-schema/kontor_schema/__init__.py | 43 ++++++++++++++----- 6 files changed, 74 insertions(+), 41 deletions(-) diff --git a/python/kontor-gui/gui/comic_window.py b/python/kontor-gui/gui/comic_window.py index 5f465d6..bf8b8d2 100644 --- a/python/kontor-gui/gui/comic_window.py +++ b/python/kontor-gui/gui/comic_window.py @@ -1,5 +1,5 @@ from PySide6.QtCore import Signal, QSortFilterProxyModel -from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QTabWidget, QMenu, QTableView, QMdiSubWindow +from PySide6.QtWidgets import QWidget, QVBoxLayout, QTabWidget, QTableView, QMdiSubWindow, QHeaderView from gui.model_config import KontorModelConfig from gui.table_model import KontorTableModel @@ -56,7 +56,8 @@ class ComicWindow(QMdiSubWindow): proxy_model = QSortFilterProxyModel() proxy_model.setSourceModel(model) table_view.setSortingEnabled(True) - #table_view.setModel(model) + header = table_view.horizontalHeader() + header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) table_view.setModel(proxy_model) layout.addLayout(table_config.get_filter_layout()) layout.addWidget(table_view) diff --git a/python/kontor-gui/gui/media_window.py b/python/kontor-gui/gui/media_window.py index 1d9455f..132a21e 100644 --- a/python/kontor-gui/gui/media_window.py +++ b/python/kontor-gui/gui/media_window.py @@ -1,5 +1,5 @@ from PySide6.QtCore import Signal, QSortFilterProxyModel -from PySide6.QtWidgets import QMdiSubWindow, QWidget, QVBoxLayout, QTabWidget, QTableView +from PySide6.QtWidgets import QMdiSubWindow, QWidget, QVBoxLayout, QTabWidget, QTableView, QHeaderView from gui.model_config import KontorModelConfig from gui.table_model import KontorTableModel @@ -57,6 +57,8 @@ class MediaWindow(QMdiSubWindow): proxy_model = QSortFilterProxyModel() proxy_model.setSourceModel(model) table_view.setSortingEnabled(True) + # header = table_view.horizontalHeader() + # header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) #table_view.setModel(model) table_view.setModel(proxy_model) layout.addLayout(table_config.get_filter_layout()) diff --git a/python/kontor-gui/gui/meta_data_window.py b/python/kontor-gui/gui/meta_data_window.py index 793fa16..49c4d73 100644 --- a/python/kontor-gui/gui/meta_data_window.py +++ b/python/kontor-gui/gui/meta_data_window.py @@ -1,5 +1,6 @@ from PySide6.QtCore import Signal, QSortFilterProxyModel -from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QTabWidget, QMenu, QTableView, QMdiSubWindow +from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QTabWidget, QMenu, QTableView, QMdiSubWindow, \ + QHeaderView from gui.model_config import KontorModelConfig from gui.table_model import KontorTableModel @@ -47,18 +48,19 @@ class MetaDataWindow(QMdiSubWindow): def generate_data_tab(self, table_name): data_tab = QWidget() - table_config = KontorModelConfig(self._main_window.kontor_db, self, table_name) model = KontorTableModel(table_config) layout = QVBoxLayout() self.data_views.append(model) data_tab.setLayout(layout) table_view = QTableView() - proxy_model = QSortFilterProxyModel() - proxy_model.setSourceModel(model) + # proxy_model = QSortFilterProxyModel() + # proxy_model.setSourceModel(model) table_view.setSortingEnabled(True) - #table_view.setModel(model) - table_view.setModel(proxy_model) + header = table_view.horizontalHeader() + header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + # table_view.setModel(proxy_model) + table_view.setModel(model) layout.addLayout(table_config.get_filter_layout()) layout.addWidget(table_view) model.refresh() diff --git a/python/kontor-gui/gui/model_config.py b/python/kontor-gui/gui/model_config.py index 7aa05cc..1259fc3 100644 --- a/python/kontor-gui/gui/model_config.py +++ b/python/kontor-gui/gui/model_config.py @@ -1,5 +1,5 @@ from PySide6.QtWidgets import QHBoxLayout, QCheckBox, QMdiSubWindow -from kontor_schema import KontorDB +from kontor_schema import KontorDB, ColumnEntry class KontorModelConfig: @@ -29,7 +29,7 @@ class KontorModelConfig: # print(self.filter["download"].isChecked()) for column, filter_info in self.filter.items(): # print(column, filter_info) - if filter_info['widget'].isChecked(): + if filter_info[ColumnEntry.COLUMN_WIDGET].isChecked(): _filters[column] = True # print(f"{filter_rule=}") # self.log.info("filters -> %s", _filters) @@ -46,9 +46,9 @@ class KontorModelConfig: filter_layout = QHBoxLayout() for column, filter_info in self.filter.items(): filter_checkbox = QCheckBox() - filter_checkbox.setText(filter_info['label']) + filter_checkbox.setText(filter_info[ColumnEntry.COLUMN_LABEL]) filter_checkbox.checkStateChanged.connect(self.main_window.refresh) - self.filter[column]['widget'] = filter_checkbox + self.filter[column][ColumnEntry.COLUMN_WIDGET] = filter_checkbox filter_layout.addWidget(filter_checkbox) filter_layout.addStretch() # self.log.info("get_filter_layout: %s", self.filter) diff --git a/python/kontor-gui/gui/table_model.py b/python/kontor-gui/gui/table_model.py index 6c7574b..345e336 100644 --- a/python/kontor-gui/gui/table_model.py +++ b/python/kontor-gui/gui/table_model.py @@ -2,6 +2,7 @@ from datetime import datetime from PySide6.QtCore import QAbstractTableModel, QModelIndex from PySide6.QtGui import Qt +from kontor_schema import ColumnEntry from .model_config import KontorModelConfig @@ -42,8 +43,9 @@ class KontorTableModel(QAbstractTableModel): return len(self._data) def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): + # self.log.info(f"{self._config.header[col]}") if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: - return self._config.header[col]['label'] + return self._config.header[col][ColumnEntry.COLUMN_LABEL] if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: return str(col+1) @@ -52,27 +54,32 @@ class KontorTableModel(QAbstractTableModel): return None value = self._data[index.row()][index.column()] # print('{}:: {}:: {}: {}'.format(index, role, value, type(value))) + row = index.row() + column = index.column() + column_type = self._config.header[column][ColumnEntry.COLUMN_TYPE] + # self.log.info(f"{row}-{column}: {column_type}") if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole: + if column_type == "BOOLEAN": + if isinstance(value, bytes): + if value == b'\x01': + return self._config.main_window.tick + else: + return self._config.main_window.cross + if isinstance(value, int): + # print('{}:: {}: {}'.format(index, value, type(value))) + if value == 1: + return self._config.main_window.tick + else: + return self._config.main_window.cross + if isinstance(value, bool): + if value: + return self._config.main_window.tick + else: + return self._config.main_window.cross if isinstance(value, datetime): return value.strftime("%Y-%m-%d %M:%M:%S") if isinstance(value, str): return value - if isinstance(value, bytes): - if value == b'\x01': - return self._config.main_window.tick - else: - return self._config.main_window.cross - if isinstance(value, int): - # print('{}:: {}: {}'.format(index, value, type(value))) - if value == 1: - return self._config.main_window.tick - else: - return self._config.main_window.cross - if isinstance(value, bool): - if value: - return self._config.main_window.tick - else: - return self._config.main_window.cross return str(value) if role == Qt.ItemDataRole.DecorationRole: if isinstance(value, bytes): diff --git a/python/kontor-schema/kontor_schema/__init__.py b/python/kontor-schema/kontor_schema/__init__.py index 5cb8bb6..0a12a3b 100644 --- a/python/kontor-schema/kontor_schema/__init__.py +++ b/python/kontor-schema/kontor_schema/__init__.py @@ -1,6 +1,7 @@ import json import uuid from datetime import datetime +from enum import Enum from logging import Logger from pathlib import Path @@ -17,6 +18,15 @@ from .tysc import Card, CardSet, Sport, Team, FieldPosition, Rooster, Player, Ve from .media import MediaFile, MediaArticle, MediaVideo +class ColumnEntry(Enum): + COLUMN_NAME = "column" + COLUMN_LABEL = 'label' + COLUMN_ORDER = 'order' + COLUMN_REF_COLUMN = 'ref_column' + COLUMN_TYPE = "type" + COLUMN_WIDGET = 'widget' + + class KontorDB: def __init__(self, db_engine: Engine, log: Logger): @@ -81,17 +91,23 @@ class KontorDB: filter(MetaDataTable.table_name == table_name). filter(MetaDataColumn.is_shown == 1).all()): # self.log.info("get_column_meta_data: %s %s %d", column.column_name, column.column_label, column.column_order) - meta_data[order] = {'column': column.column_name, 'label': column.column_label, - 'order': column.column_order, 'ref_column': column.ref_column} + meta_data[order] = { + ColumnEntry.COLUMN_NAME: column.column_name, + ColumnEntry.COLUMN_LABEL: column.column_label, + ColumnEntry.COLUMN_ORDER: column.column_order, + ColumnEntry.COLUMN_REF_COLUMN: column.ref_column, + ColumnEntry.COLUMN_TYPE: column.column_type + } order += 1 else: for (_, column) in (session.query(MetaDataTable, MetaDataColumn). filter(MetaDataTable.id == MetaDataColumn.table_id). filter(MetaDataTable.table_name == table_name).all()): meta_data[order] = { - 'column': column.column_name, - 'order': column.column_order, - 'ref_column': column.ref_column + ColumnEntry.COLUMN_NAME: column.column_name, + ColumnEntry.COLUMN_ORDER: column.column_order, + ColumnEntry.COLUMN_REF_COLUMN: column.ref_column, + ColumnEntry.COLUMN_TYPE: column.column_type } order += 1 # self.log.info("get_column_meta_data: %s", meta_data) @@ -99,13 +115,15 @@ class KontorDB: def get_columns(self, table_name: str) -> dict: columns = {} - order = 0 __session__ = sessionmaker(self.engine) with __session__() as session: for (_, column) in (session.query(MetaDataTable, MetaDataColumn). filter(MetaDataTable.id == MetaDataColumn.table_id). filter(MetaDataTable.table_name == table_name).all()): - columns[column.column_name] = {"order": column.column_order, "type": column.column_type} + columns[column.column_name] = { + ColumnEntry.COLUMN_ORDER: column.column_order, + ColumnEntry.COLUMN_TYPE: column.column_type + } return columns def get_filters(self, table_name: str) -> dict: @@ -116,7 +134,10 @@ class KontorDB: filter(MetaDataTable.id == MetaDataColumn.table_id). filter(MetaDataTable.table_name == table_name). filter(MetaDataColumn.show_filter == 1).all()): - _filter_map[column.column_name] = {'label': column.filter_label, 'widget': None} + _filter_map[column.column_name] = { + ColumnEntry.COLUMN_LABEL: column.filter_label, + ColumnEntry.COLUMN_WIDGET: None + } return _filter_map def data(self, table_name: str, columns: dict, filters: dict) -> list: @@ -133,8 +154,8 @@ class KontorDB: # self.log.info("data: %s", entry) row = [] for order in columns.keys(): - column_name = columns[order]['column'] - ref_column = columns[order]['ref_column'] + column_name = columns[order][ColumnEntry.COLUMN_NAME] + ref_column = columns[order][ColumnEntry.COLUMN_REF_COLUMN] if str(column_name).endswith("_id"): ref_table = column_name[:-3] ref = getattr(entry, ref_table) @@ -166,7 +187,7 @@ class KontorDB: entry = {} for order in columns: # print(columns[order]) - column_name = columns[order]['column'] + column_name = columns[order][ColumnEntry.COLUMN_NAME] # print(f"get value {column_name} from {row} of table {table}") try: value = getattr(row, column_name) From 1a5cd6ffe80d3fc4ea38e35c5df68edc4c25e3e6 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Thu, 6 Feb 2025 11:09:09 +0100 Subject: [PATCH 76/91] refactor kontor-schema by moving classes to seprate modules --- .../kontor-cli/kontor/controllers/__init__.py | 7 + .../kontor-cli/kontor/controllers/database.py | 30 +- .../kontor/templates/export_db.jinja2 | 5 - python/kontor-gui/gui/main_window.py | 2 +- python/kontor-gui/gui/model_config.py | 3 +- python/kontor-gui/gui/table_model.py | 3 +- .../kontor-schema/kontor_schema/__init__.py | 353 +--------------- .../kontor-schema/kontor_schema/database.py | 388 ++++++++++++++++++ 8 files changed, 420 insertions(+), 371 deletions(-) delete mode 100644 python/kontor-cli/kontor/templates/export_db.jinja2 create mode 100644 python/kontor-schema/kontor_schema/database.py diff --git a/python/kontor-cli/kontor/controllers/__init__.py b/python/kontor-cli/kontor/controllers/__init__.py index e69de29..3f2fa1c 100644 --- a/python/kontor-cli/kontor/controllers/__init__.py +++ b/python/kontor-cli/kontor/controllers/__init__.py @@ -0,0 +1,7 @@ +from enum import Enum + +class ArgumentData(Enum): + EXPORT_TYPE = 'export_type' + DB_FILE = 'db_file' + DATA_TYPE = 'data_type' + DELETE_FIRST = 'delete_first' diff --git a/python/kontor-cli/kontor/controllers/database.py b/python/kontor-cli/kontor/controllers/database.py index c748f75..b5973d2 100644 --- a/python/kontor-cli/kontor/controllers/database.py +++ b/python/kontor-cli/kontor/controllers/database.py @@ -1,6 +1,8 @@ import mariadb from cement import Controller, ex +from kontor.controllers import ArgumentData + class Database(Controller): class Meta: @@ -24,16 +26,16 @@ class Database(Controller): ) def export(self): data = { - 'db_file': 'data.json', - 'export_type': 'JSON', + ArgumentData.DB_FILE: 'data.json', + ArgumentData.EXPORT_TYPE: 'JSON', } if self.app.pargs.db_file is not None: - data['db_file'] = self.app.pargs.db_file + data[ArgumentData.DB_FILE] = self.app.pargs.db_file db = self.app.kontor_db - self.app.log.info(f"export DB to {data['db_file']} as {data['export_type']}") - results = db.export_db(data['export_type'], data['db_file']) - data['results'] = results - self.app.render(data, 'export_db.jinja2') + self.app.log.info(f"export DB to {data[ArgumentData.DB_FILE]} as {data[ArgumentData.EXPORT_TYPE]}") + results = db.export_db(data[ArgumentData.EXPORT_TYPE], data[ArgumentData.DB_FILE]) + for key, value in results.items(): + self.app.log.info(f"{key}: {value}") @ex( label='import', @@ -51,16 +53,18 @@ class Database(Controller): ) def import_cmd(self): data = { - 'db_file': 'data.json', - 'data_type': 'JSON', - 'delete_first': False, + ArgumentData.DB_FILE: 'data.json', + ArgumentData.DATA_TYPE: 'JSON', + ArgumentData.DELETE_FIRST: False, } if self.app.pargs.db_file is not None: - data['db_file'] = self.app.pargs.db_file + data[ArgumentData.DB_FILE] = self.app.pargs.db_file if self.app.pargs.delete_first is not None: - data['delete_first'] = self.app.pargs.delete_first + data[ArgumentData.DELETE_FIRST] = self.app.pargs.delete_first db = self.app.kontor_db - db.import_db(data['db_file'], data['delete_first']) + if data[ArgumentData.DELETE_FIRST]: + db.delete_entries() + db.import_db(data[ArgumentData.DB_FILE]) @ex( help='check the db schema against MetaDataTable and MetaDataColumn' diff --git a/python/kontor-cli/kontor/templates/export_db.jinja2 b/python/kontor-cli/kontor/templates/export_db.jinja2 deleted file mode 100644 index 678aaa3..0000000 --- a/python/kontor-cli/kontor/templates/export_db.jinja2 +++ /dev/null @@ -1,5 +0,0 @@ - Following tables were exported: - -{% for key, value in results.items() %} -Table {{key}}: {{value}} entries -{% endfor %} diff --git a/python/kontor-gui/gui/main_window.py b/python/kontor-gui/gui/main_window.py index a1921d7..e30727a 100644 --- a/python/kontor-gui/gui/main_window.py +++ b/python/kontor-gui/gui/main_window.py @@ -181,7 +181,7 @@ class MainWindow(QMainWindow): import_dlg = ImportKontorDialog(self) if import_dlg.exec(): print(f"import DB from file {import_dlg.file_name}") - self.kontor_db.import_db(import_dlg.file_name, False) + self.kontor_db.import_db(import_dlg.file_name) else: print("do nothing for import") diff --git a/python/kontor-gui/gui/model_config.py b/python/kontor-gui/gui/model_config.py index 7aa05cc..990d3e4 100644 --- a/python/kontor-gui/gui/model_config.py +++ b/python/kontor-gui/gui/model_config.py @@ -1,5 +1,6 @@ from PySide6.QtWidgets import QHBoxLayout, QCheckBox, QMdiSubWindow from kontor_schema import KontorDB +from kontor_schema.database import ColumnEntry class KontorModelConfig: @@ -46,7 +47,7 @@ class KontorModelConfig: filter_layout = QHBoxLayout() for column, filter_info in self.filter.items(): filter_checkbox = QCheckBox() - filter_checkbox.setText(filter_info['label']) + filter_checkbox.setText(filter_info[ColumnEntry.COLUMN_LABEL]) filter_checkbox.checkStateChanged.connect(self.main_window.refresh) self.filter[column]['widget'] = filter_checkbox filter_layout.addWidget(filter_checkbox) diff --git a/python/kontor-gui/gui/table_model.py b/python/kontor-gui/gui/table_model.py index 6c7574b..59340a9 100644 --- a/python/kontor-gui/gui/table_model.py +++ b/python/kontor-gui/gui/table_model.py @@ -2,6 +2,7 @@ from datetime import datetime from PySide6.QtCore import QAbstractTableModel, QModelIndex from PySide6.QtGui import Qt +from kontor_schema.database import ColumnEntry from .model_config import KontorModelConfig @@ -43,7 +44,7 @@ class KontorTableModel(QAbstractTableModel): def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: - return self._config.header[col]['label'] + return self._config.header[col][ColumnEntry.COLUMN_LABEL] if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: return str(col+1) diff --git a/python/kontor-schema/kontor_schema/__init__.py b/python/kontor-schema/kontor_schema/__init__.py index 5cb8bb6..827b402 100644 --- a/python/kontor-schema/kontor_schema/__init__.py +++ b/python/kontor-schema/kontor_schema/__init__.py @@ -1,357 +1,10 @@ -import json -import uuid -from datetime import datetime -from logging import Logger -from pathlib import Path +from enum import Enum, auto -from sqlalchemy import Engine, select -from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import sessionmaker - -from .base import Base, BaseMixin from .admin import User, Token, Role, AuthorizationMatrix, ModuleData, MailAccount, Mail from .bookshelf import Article, Book, Author, BookshelfPublisher, ArticleAuthor, BookAuthor from .comic import Comic, Artist, Publisher, Issue, StoryArc, TradePaperback, Volume, ComicWork, WorkType from .metadata import MetaDataTable, MetaDataColumn from .tysc import Card, CardSet, Sport, Team, FieldPosition, Rooster, Player, Vendor from .media import MediaFile, MediaArticle, MediaVideo - - -class KontorDB: - - def __init__(self, db_engine: Engine, log: Logger): - self.engine = db_engine - self.registry = {} - self.init_registry() - self.log = log - - def init_registry(self): - self.registry[Card.__tablename__] = Card - self.registry[CardSet.__tablename__] = CardSet - self.registry[Rooster.__tablename__] = Rooster - self.registry[Team.__tablename__] = Team - self.registry[FieldPosition.__tablename__] = FieldPosition - self.registry[Player.__tablename__] = Player - self.registry[Vendor.__tablename__] = Vendor - self.registry[Sport.__tablename__] = Sport - self.registry[Issue.__tablename__] = Issue - self.registry[TradePaperback.__tablename__] = TradePaperback - self.registry[StoryArc.__tablename__] = StoryArc - self.registry[Volume.__tablename__] = Volume - self.registry[ComicWork.__tablename__] = ComicWork - self.registry[Artist.__tablename__] = Artist - self.registry[Comic.__tablename__] = Comic - self.registry[Publisher.__tablename__] = Publisher - self.registry[WorkType.__tablename__] = WorkType - self.registry[ArticleAuthor.__tablename__] = ArticleAuthor - self.registry[BookAuthor.__tablename__] = BookAuthor - self.registry[BookshelfPublisher.__tablename__] = BookshelfPublisher - self.registry[Article.__tablename__] = Article - self.registry[Book.__tablename__] = Book - self.registry[Author.__tablename__] = Author - self.registry[MediaFile.__tablename__] = MediaFile - self.registry[MediaArticle.__tablename__] = MediaArticle - self.registry[MediaVideo.__tablename__] = MediaVideo - self.registry[MetaDataColumn.__tablename__] = MetaDataColumn - self.registry[MetaDataTable.__tablename__] = MetaDataTable - self.registry[AuthorizationMatrix.__tablename__] = AuthorizationMatrix - self.registry[Token.__tablename__] = Token - self.registry[User.__tablename__] = User - self.registry[Role.__tablename__] = Role - self.registry[ModuleData.__tablename__] = ModuleData - self.registry[MailAccount.__tablename__] = MailAccount - self.registry[Mail.__tablename__] = Mail - - def get_table_names(self) -> list: - result = [] - __session__ = sessionmaker(self.engine) - with __session__() as session: - tables = session.scalars(select(MetaDataTable)).all() - result = [table.table_name for table in tables] - return result - - def get_column_meta_data(self, table_name: str, view_only=True) -> dict: - meta_data = {} - order = 0 - __session__ = sessionmaker(self.engine) - with __session__() as session: - if view_only: - for (_, column) in (session.query(MetaDataTable, MetaDataColumn). - filter(MetaDataTable.id == MetaDataColumn.table_id). - filter(MetaDataTable.table_name == table_name). - filter(MetaDataColumn.is_shown == 1).all()): - # self.log.info("get_column_meta_data: %s %s %d", column.column_name, column.column_label, column.column_order) - meta_data[order] = {'column': column.column_name, 'label': column.column_label, - 'order': column.column_order, 'ref_column': column.ref_column} - order += 1 - else: - for (_, column) in (session.query(MetaDataTable, MetaDataColumn). - filter(MetaDataTable.id == MetaDataColumn.table_id). - filter(MetaDataTable.table_name == table_name).all()): - meta_data[order] = { - 'column': column.column_name, - 'order': column.column_order, - 'ref_column': column.ref_column - } - order += 1 - # self.log.info("get_column_meta_data: %s", meta_data) - return meta_data - - def get_columns(self, table_name: str) -> dict: - columns = {} - order = 0 - __session__ = sessionmaker(self.engine) - with __session__() as session: - for (_, column) in (session.query(MetaDataTable, MetaDataColumn). - filter(MetaDataTable.id == MetaDataColumn.table_id). - filter(MetaDataTable.table_name == table_name).all()): - columns[column.column_name] = {"order": column.column_order, "type": column.column_type} - return columns - - def get_filters(self, table_name: str) -> dict: - _filter_map = {} - __session__ = sessionmaker(self.engine) - with __session__() as session: - for (_, column) in (session.query(MetaDataTable, MetaDataColumn). - filter(MetaDataTable.id == MetaDataColumn.table_id). - filter(MetaDataTable.table_name == table_name). - filter(MetaDataColumn.show_filter == 1).all()): - _filter_map[column.column_name] = {'label': column.filter_label, 'widget': None} - return _filter_map - - def data(self, table_name: str, columns: dict, filters: dict) -> list: - data = [] - __session__ = sessionmaker(self.engine) - table = self.registry[table_name] - with __session__() as session: - entries = [] - if len(filters) == 0: - entries = session.scalars(select(table)).all() - else: - entries = session.scalars(select(table).filter_by(**filters)).all() - for entry in entries: - # self.log.info("data: %s", entry) - row = [] - for order in columns.keys(): - column_name = columns[order]['column'] - ref_column = columns[order]['ref_column'] - if str(column_name).endswith("_id"): - ref_table = column_name[:-3] - ref = getattr(entry, ref_table) - value = getattr(ref, ref_column) - row.append(value) - else: - row.append(getattr(entry, column_name)) - data.append(row) - # self.log.info("data: %s", data) - return data - - def export_db(self, export_type: str, export_file_name: str) -> dict: - results = {} - db = {} - export_table_list = self.get_table_names() - for table in export_table_list: - columns = self.get_column_meta_data(table, view_only=False) - if table in self.registry: - model = self.registry[table] - else: - self.log.info(f"table {table} is not registered") - continue - __session__ = sessionmaker(self.engine) - with __session__() as session: - rows = session.query(model).all() - entries = [] - for row in rows: - # print(row) - entry = {} - for order in columns: - # print(columns[order]) - column_name = columns[order]['column'] - # print(f"get value {column_name} from {row} of table {table}") - try: - value = getattr(row, column_name) - if isinstance(value, datetime): - entry[column_name] = str(value) - else: - entry[column_name] = value - except AttributeError: - pass - entries.append(entry) - db[table] = entries - results[table] = len(entries) - match export_type: - case "JSON": - json_dump = json.dumps(db, indent=4) - with open(export_file_name, "w") as dump_file: - dump_file.write(json_dump) - case "YAML": - export_file = Path(export_file_name) - case "SQLite": - export_file = Path(export_file_name) - self.log.info("%d tables exported", len(results)) - return results - - def import_db(self, import_file_name: str, delete_first: bool) -> dict: - result = {} - if delete_first: - self.delete_entries() - import_file = Path(import_file_name) - if not import_file.exists(): - self.log.info(f"File {import_file_name} does not exist. Do nothing.") - return result - match import_file.suffix: - case '.json': - print("read json file") - with open(import_file_name, 'r') as json_file: - json_load = json.load(json_file) - for table in json_load: - self.log.info(f"{table}: {len(json_load[table])}") - result[table] = self.import_table(table, json_load[table]) - case '.yml': - print("read yaml file") - case '.yaml': - print("read yaml file") - case '.db': - print("read sqlite file") - return result - - def import_table(self, table_name: str, items:list) -> dict: - result = {} - updated = [] - added = [] - remaining = [] - existing_ids = self.get_ids(table_name) - self.log.info(f"found {len(existing_ids)} existing ids for table {table_name}") - for item in items: - current_id = item['id'] - # print(f"import item: {item}") - found_item = None - __session__ = sessionmaker(self.engine) - with __session__() as session: - found_item = session.get(self.registry[table_name], current_id) - # print(f"found item: {found_item}") - if found_item is not None: - changed = self.update_entry(table_name, current_id, item) - updated.append(item) - if changed: - self.log.info(f"{current_id} has changed") - updated.append(item) - existing_ids.remove(current_id) - else: - try: - self.add_entry(table_name, item) - added.append(item) - except IntegrityError as error: - self.log.info(f"Could not add item, due to: {error.detail}") - if len(existing_ids) > 0: - print(f"remaining items: {existing_ids}") - remaining.extend(existing_ids) - result['updated'] = updated - result['added'] = added - result['remaining'] = remaining - return result - - def get_ids(self, table_name: str) -> list: - existing_ids = [] - __session__ = sessionmaker(self.engine) - with __session__() as session: - items = session.query(self.registry[table_name]).all() - for item in items: - existing_ids.append(getattr(item, 'id')) - return existing_ids - - def add_entry(self, table_name: str, update_item: dict): - self.log.debug(f"add entry to table {table_name} with {update_item}") - __session__ = sessionmaker(self.engine) - with __session__() as session: - add_item = self.registry[table_name]() - for key in update_item.keys(): - update_value = update_item[key] - setattr(add_item, key, update_value) - session.add(add_item) - session.commit() - - def update_entry(self, table_name, current_id, update_item: dict) -> bool: - # self.log.info("update entry to table %s", table_name) - __session__ = sessionmaker(self.engine) - with __session__() as session: - existing_item = session.query(self.registry[table_name]).get(current_id) - changed = False - for key in update_item.keys(): - update_value = update_item[key] - existing_value = getattr(existing_item, key) - if type(existing_value) is not type(update_value): - existing_value = str(existing_value) - if existing_value != update_value: - self.log.info(f"{key} has changed: {existing_value} != {update_value}") - setattr(existing_item, key, update_value) - session.commit() - changed = True - self.log.info(f"update {key} with {update_value}") - return changed - - def add_link(self, link: str) -> dict: - result = {} - __session__ = sessionmaker(self.engine) - with __session__() as session: - media_file = MediaFile() - media_file.id = str(uuid.uuid4()) - media_file.created_date = datetime.now() - media_file.last_modified_date = datetime.now() - media_file.version = 0 - media_file.url = link - media_file.review = 1 - media_file.should_download = 1 - try: - session.add(media_file) - session.commit() - result['added'] = {'url': media_file.url, 'title': media_file.title, 'review': media_file.review, 'download': media_file.should_download} - except IntegrityError as error: - session.rollback() - result['error'] = error.orig - return result - - def update_titles(self) -> dict: - update_list = {} - __session__ = sessionmaker(self.engine) - with __session__() as session: - links = session.query(MediaFile).filter(MediaFile.review == 1).all() - for link in links: - url = link.url - if url is None: - continue - link.update_title() - session.commit() - update_list[link.id] = link.title - return update_list - - def get_download_list(self) -> list: - download_list = [] - __session__ = sessionmaker(self.engine) - with __session__() as session: - links = session.query(MediaFile).filter(MediaFile.should_download == 1).all() - for link in links: - url = link.url - if url is None: - continue - download_list.append(link.id) - return download_list - - def download_file(self, entry_id: str, download_dir = "/data/media", dl_tool = "yt-dlp") -> str: - __session__ = sessionmaker(self.engine) - with __session__() as session: - link = session.query(MediaFile).get(entry_id) - link.download_file(download_dir, dl_tool) - session.commit() - file_name = link.file_name - return file_name - - def delete_entries(self): - for (table_name, table) in self.registry.items(): - # self.log.info("delete entries from table %s", table_name) - __session__ = sessionmaker(self.engine) - with __session__() as session: - items = session.query(table).all() - for item in items: - session.delete(item) - session.commit() +from .base import Base +from .database import KontorDB diff --git a/python/kontor-schema/kontor_schema/database.py b/python/kontor-schema/kontor_schema/database.py new file mode 100644 index 0000000..f722ef3 --- /dev/null +++ b/python/kontor-schema/kontor_schema/database.py @@ -0,0 +1,388 @@ +import json +import uuid +from datetime import datetime +from enum import Enum, auto +from logging import Logger +from pathlib import Path + +from sqlalchemy import Engine, select +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import sessionmaker + +from .tysc import Card, CardSet, Rooster, Team, FieldPosition, Player, Vendor, Sport +from .comic import Issue, TradePaperback, StoryArc, Volume, ComicWork, Artist, Comic, Publisher, WorkType +from .bookshelf import ArticleAuthor, BookAuthor, BookshelfPublisher, Article, Book, Author +from .admin import Mail, MailAccount, ModuleData, Role, User, Token, AuthorizationMatrix +from .metadata import MetaDataTable, MetaDataColumn +from .media import MediaVideo, MediaArticle, MediaFile + + +class ColumnEntry(Enum): + COLUMN_NAME = 'column' + COLUMN_LABEL = 'label' + COLUMN_ORDER = 'order' + COLUMN_REF_COLUMN = 'ref_column' + COLUMN_TYPE = 'type' + COLUMN_WIDGET = 'widget' + + +class StatusType(Enum): + UNKNOWN = auto() + FILE_NAME = auto() + FILE_ID = auto() + DUPLICATE = auto() + CLOUD_LINK = auto() + CLOUD_LINK_ID = auto() + + +class KontorDB: + + def __init__(self, db_engine: Engine, log: Logger): + self.engine = db_engine + self.registry = {} + self.init_registry() + self.log = log + + def init_registry(self): + self.registry[Card.__tablename__] = Card + self.registry[CardSet.__tablename__] = CardSet + self.registry[Rooster.__tablename__] = Rooster + self.registry[Team.__tablename__] = Team + self.registry[FieldPosition.__tablename__] = FieldPosition + self.registry[Player.__tablename__] = Player + self.registry[Vendor.__tablename__] = Vendor + self.registry[Sport.__tablename__] = Sport + self.registry[Issue.__tablename__] = Issue + self.registry[TradePaperback.__tablename__] = TradePaperback + self.registry[StoryArc.__tablename__] = StoryArc + self.registry[Volume.__tablename__] = Volume + self.registry[ComicWork.__tablename__] = ComicWork + self.registry[Artist.__tablename__] = Artist + self.registry[Comic.__tablename__] = Comic + self.registry[Publisher.__tablename__] = Publisher + self.registry[WorkType.__tablename__] = WorkType + self.registry[ArticleAuthor.__tablename__] = ArticleAuthor + self.registry[BookAuthor.__tablename__] = BookAuthor + self.registry[BookshelfPublisher.__tablename__] = BookshelfPublisher + self.registry[Article.__tablename__] = Article + self.registry[Book.__tablename__] = Book + self.registry[Author.__tablename__] = Author + self.registry[MediaFile.__tablename__] = MediaFile + self.registry[MediaArticle.__tablename__] = MediaArticle + self.registry[MediaVideo.__tablename__] = MediaVideo + self.registry[MetaDataColumn.__tablename__] = MetaDataColumn + self.registry[MetaDataTable.__tablename__] = MetaDataTable + self.registry[AuthorizationMatrix.__tablename__] = AuthorizationMatrix + self.registry[Token.__tablename__] = Token + self.registry[User.__tablename__] = User + self.registry[Role.__tablename__] = Role + self.registry[ModuleData.__tablename__] = ModuleData + self.registry[MailAccount.__tablename__] = MailAccount + self.registry[Mail.__tablename__] = Mail + + def get_table_names(self) -> list: + result = [] + __session__ = sessionmaker(self.engine) + with __session__() as session: + tables = session.scalars(select(MetaDataTable)).all() + result = [table.table_name for table in tables] + return result + + def get_column_meta_data(self, table_name: str, view_only=True) -> dict: + meta_data = {} + order = 0 + __session__ = sessionmaker(self.engine) + with __session__() as session: + if view_only: + for (_, column) in (session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name). + filter(MetaDataColumn.is_shown == 1).all()): + # self.log.info("get_column_meta_data: %s %s %d", column.column_name, column.column_label, column.column_order) + meta_data[order] = { + ColumnEntry.COLUMN_NAME: column.column_name, + ColumnEntry.COLUMN_LABEL: column.column_label, + ColumnEntry.COLUMN_ORDER: column.column_order, + ColumnEntry.COLUMN_REF_COLUMN: column.ref_column, + ColumnEntry.COLUMN_TYPE: column.column_type + } + order += 1 + else: + for (_, column) in (session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name).all()): + meta_data[order] = { + ColumnEntry.COLUMN_NAME: column.column_name, + ColumnEntry.COLUMN_ORDER: column.column_order, + ColumnEntry.COLUMN_REF_COLUMN: column.ref_column, + ColumnEntry.COLUMN_TYPE: column.column_type + } + order += 1 + # self.log.info("get_column_meta_data: %s", meta_data) + return meta_data + + def get_columns(self, table_name: str) -> dict: + columns = {} + order = 0 + __session__ = sessionmaker(self.engine) + with __session__() as session: + for (_, column) in (session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name).all()): + columns[column.column_name] = { + ColumnEntry.COLUMN_ORDER: column.column_order, + ColumnEntry.COLUMN_TYPE: column.column_type + } + return columns + + def get_filters(self, table_name: str) -> dict: + _filter_map = {} + __session__ = sessionmaker(self.engine) + with __session__() as session: + for (_, column) in (session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name). + filter(MetaDataColumn.show_filter == 1).all()): + _filter_map[column.column_name] = { + ColumnEntry.COLUMN_LABEL: column.filter_label, + ColumnEntry.COLUMN_WIDGET: None + } + return _filter_map + + def data(self, table_name: str, columns: dict, filters: dict) -> list: + data = [] + __session__ = sessionmaker(self.engine) + table = self.registry[table_name] + with __session__() as session: + entries = [] + if len(filters) == 0: + entries = session.scalars(select(table)).all() + else: + entries = session.scalars(select(table).filter_by(**filters)).all() + for entry in entries: + # self.log.info("data: %s", entry) + row = [] + for order in columns.keys(): + column_name = columns[order][ColumnEntry.COLUMN_NAME] + ref_column = columns[order][ColumnEntry.COLUMN_REF_COLUMN] + if str(column_name).endswith("_id"): + ref_table = column_name[:-3] + ref = getattr(entry, ref_table) + value = getattr(ref, ref_column) + row.append(value) + else: + row.append(getattr(entry, column_name)) + data.append(row) + # self.log.info("data: %s", data) + return data + + def export_db(self, export_type: str, export_file_name: str) -> dict: + results = {} + db = {} + export_table_list = self.get_table_names() + for table in export_table_list: + columns = self.get_column_meta_data(table, view_only=False) + if table in self.registry: + model = self.registry[table] + else: + self.log.info(f"table {table} is not registered") + continue + __session__ = sessionmaker(self.engine) + with __session__() as session: + rows = session.query(model).all() + entries = [] + for row in rows: + # print(row) + entry = {} + for order in columns: + # print(columns[order]) + column_name = columns[order][ColumnEntry.COLUMN_NAME] + # print(f"get value {column_name} from {row} of table {table}") + try: + value = getattr(row, column_name) + if isinstance(value, datetime): + entry[column_name] = str(value) + else: + entry[column_name] = value + except AttributeError: + pass + entries.append(entry) + db[table] = entries + results[table] = len(entries) + match export_type: + case "JSON": + json_dump = json.dumps(db, indent=4) + with open(export_file_name, "w") as dump_file: + dump_file.write(json_dump) + case "YAML": + export_file = Path(export_file_name) + case "SQLite": + export_file = Path(export_file_name) + self.log.info("%d tables exported", len(results)) + return results + + def import_db(self, import_file_name: str) -> dict: + result = {} + import_file = Path(import_file_name) + if not import_file.exists(): + self.log.info(f"File {import_file_name} does not exist. Do nothing.") + return result + match import_file.suffix: + case '.json': + print("read json file") + with open(import_file_name, 'r') as json_file: + json_load = json.load(json_file) + for table in json_load: + self.log.info(f"{table}: {len(json_load[table])}") + result[table] = self.import_table(table, json_load[table]) + case '.yml': + print("read yaml file") + case '.yaml': + print("read yaml file") + case '.db': + print("read sqlite file") + return result + + def import_table(self, table_name: str, items:list) -> dict: + result = {} + updated = [] + added = [] + remaining = [] + existing_ids = self.get_ids(table_name) + self.log.info(f"found {len(existing_ids)} existing ids for table {table_name}") + for item in items: + current_id = item['id'] + # print(f"import item: {item}") + found_item = None + __session__ = sessionmaker(self.engine) + with __session__() as session: + found_item = session.get(self.registry[table_name], current_id) + # print(f"found item: {found_item}") + if found_item is not None: + changed = self.update_entry(table_name, current_id, item) + updated.append(item) + if changed: + self.log.info(f"{current_id} has changed") + updated.append(item) + existing_ids.remove(current_id) + else: + try: + self.add_entry(table_name, item) + added.append(item) + except IntegrityError as error: + self.log.info(f"Could not add item, due to: {error.detail}") + if len(existing_ids) > 0: + print(f"remaining items: {existing_ids}") + remaining.extend(existing_ids) + result['updated'] = updated + result['added'] = added + result['remaining'] = remaining + return result + + def get_ids(self, table_name: str) -> list: + existing_ids = [] + __session__ = sessionmaker(self.engine) + with __session__() as session: + items = session.query(self.registry[table_name]).all() + for item in items: + existing_ids.append(getattr(item, 'id')) + return existing_ids + + def add_entry(self, table_name: str, update_item: dict): + self.log.debug(f"add entry to table {table_name} with {update_item}") + __session__ = sessionmaker(self.engine) + with __session__() as session: + add_item = self.registry[table_name]() + for key in update_item.keys(): + update_value = update_item[key] + setattr(add_item, key, update_value) + session.add(add_item) + session.commit() + + def update_entry(self, table_name, current_id, update_item: dict) -> bool: + # self.log.info("update entry to table %s", table_name) + __session__ = sessionmaker(self.engine) + with __session__() as session: + existing_item = session.query(self.registry[table_name]).get(current_id) + changed = False + for key in update_item.keys(): + update_value = update_item[key] + existing_value = getattr(existing_item, key) + if type(existing_value) is not type(update_value): + existing_value = str(existing_value) + if existing_value != update_value: + self.log.info(f"{key} has changed: {existing_value} != {update_value}") + setattr(existing_item, key, update_value) + session.commit() + changed = True + self.log.info(f"update {key} with {update_value}") + return changed + + def add_link(self, link: str) -> dict: + result = {} + __session__ = sessionmaker(self.engine) + with __session__() as session: + media_file = MediaFile() + media_file.id = str(uuid.uuid4()) + media_file.created_date = datetime.now() + media_file.last_modified_date = datetime.now() + media_file.version = 0 + media_file.url = link + media_file.review = 1 + media_file.should_download = 1 + try: + session.add(media_file) + session.commit() + result['added'] = {'url': media_file.url, 'title': media_file.title, 'review': media_file.review, 'download': media_file.should_download} + except IntegrityError as error: + session.rollback() + result['error'] = error.orig + return result + + def update_titles(self) -> dict: + update_list = {} + __session__ = sessionmaker(self.engine) + with __session__() as session: + links = session.query(MediaFile).filter(MediaFile.review == 1).all() + for link in links: + url = link.url + if url is None: + continue + link.update_title() + session.commit() + update_list[link.id] = link.title + return update_list + + def get_download_list(self) -> list: + download_list = [] + __session__ = sessionmaker(self.engine) + with __session__() as session: + links = session.query(MediaFile).filter(MediaFile.should_download == 1).all() + for link in links: + url = link.url + if url is None: + continue + download_list.append(link.id) + return download_list + + def download_file(self, entry_id: str, download_dir = "/data/media", dl_tool = "yt-dlp") -> str: + __session__ = sessionmaker(self.engine) + with __session__() as session: + link = session.query(MediaFile).get(entry_id) + link.download_file(download_dir, dl_tool) + session.commit() + file_name = link.file_name + return file_name + + def delete_entries(self): + for (table_name, table) in self.registry.items(): + # self.log.info("delete entries from table %s", table_name) + __session__ = sessionmaker(self.engine) + with __session__() as session: + items = session.query(table).all() + for item in items: + session.delete(item) + session.commit() + + def check_files(self): + pass From 171bc1676a1e654abfecbca4af365fcc940f7d0d Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Thu, 6 Feb 2025 16:33:15 +0100 Subject: [PATCH 77/91] refactor project by using enums for recurring strings --- python/kontor-gui/gui/comic_window.py | 6 +- python/kontor-gui/gui/media_window.py | 2 +- python/kontor-gui/gui/meta_data_window.py | 7 +- python/kontor-gui/gui/model_config.py | 7 +- python/kontor-gui/gui/table_model.py | 96 ++++++++++--------- .../kontor-schema/kontor_schema/__init__.py | 2 +- 6 files changed, 65 insertions(+), 55 deletions(-) diff --git a/python/kontor-gui/gui/comic_window.py b/python/kontor-gui/gui/comic_window.py index 5f465d6..c4a9064 100644 --- a/python/kontor-gui/gui/comic_window.py +++ b/python/kontor-gui/gui/comic_window.py @@ -1,5 +1,6 @@ from PySide6.QtCore import Signal, QSortFilterProxyModel -from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QTabWidget, QMenu, QTableView, QMdiSubWindow +from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QTabWidget, QMenu, QTableView, QMdiSubWindow, \ + QHeaderView from gui.model_config import KontorModelConfig from gui.table_model import KontorTableModel @@ -53,10 +54,11 @@ class ComicWindow(QMdiSubWindow): self.data_views.append(model) data_tab.setLayout(layout) table_view = QTableView() + header = table_view.horizontalHeader() + header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) proxy_model = QSortFilterProxyModel() proxy_model.setSourceModel(model) table_view.setSortingEnabled(True) - #table_view.setModel(model) table_view.setModel(proxy_model) layout.addLayout(table_config.get_filter_layout()) layout.addWidget(table_view) diff --git a/python/kontor-gui/gui/media_window.py b/python/kontor-gui/gui/media_window.py index 1d9455f..a88e46c 100644 --- a/python/kontor-gui/gui/media_window.py +++ b/python/kontor-gui/gui/media_window.py @@ -57,9 +57,9 @@ class MediaWindow(QMdiSubWindow): proxy_model = QSortFilterProxyModel() proxy_model.setSourceModel(model) table_view.setSortingEnabled(True) - #table_view.setModel(model) table_view.setModel(proxy_model) layout.addLayout(table_config.get_filter_layout()) layout.addWidget(table_view) model.refresh() + table_view.resizeColumnToContents(0) return data_tab diff --git a/python/kontor-gui/gui/meta_data_window.py b/python/kontor-gui/gui/meta_data_window.py index 793fa16..e9546fd 100644 --- a/python/kontor-gui/gui/meta_data_window.py +++ b/python/kontor-gui/gui/meta_data_window.py @@ -1,5 +1,6 @@ from PySide6.QtCore import Signal, QSortFilterProxyModel -from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QTabWidget, QMenu, QTableView, QMdiSubWindow +from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QTabWidget, QMenu, QTableView, QMdiSubWindow, \ + QHeaderView from gui.model_config import KontorModelConfig from gui.table_model import KontorTableModel @@ -57,9 +58,11 @@ class MetaDataWindow(QMdiSubWindow): proxy_model = QSortFilterProxyModel() proxy_model.setSourceModel(model) table_view.setSortingEnabled(True) - #table_view.setModel(model) + # header = table_view.horizontalHeader() + # header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) table_view.setModel(proxy_model) layout.addLayout(table_config.get_filter_layout()) layout.addWidget(table_view) model.refresh() + table_view.resizeColumnToContents(0) return data_tab diff --git a/python/kontor-gui/gui/model_config.py b/python/kontor-gui/gui/model_config.py index 990d3e4..1259fc3 100644 --- a/python/kontor-gui/gui/model_config.py +++ b/python/kontor-gui/gui/model_config.py @@ -1,6 +1,5 @@ from PySide6.QtWidgets import QHBoxLayout, QCheckBox, QMdiSubWindow -from kontor_schema import KontorDB -from kontor_schema.database import ColumnEntry +from kontor_schema import KontorDB, ColumnEntry class KontorModelConfig: @@ -30,7 +29,7 @@ class KontorModelConfig: # print(self.filter["download"].isChecked()) for column, filter_info in self.filter.items(): # print(column, filter_info) - if filter_info['widget'].isChecked(): + if filter_info[ColumnEntry.COLUMN_WIDGET].isChecked(): _filters[column] = True # print(f"{filter_rule=}") # self.log.info("filters -> %s", _filters) @@ -49,7 +48,7 @@ class KontorModelConfig: filter_checkbox = QCheckBox() filter_checkbox.setText(filter_info[ColumnEntry.COLUMN_LABEL]) filter_checkbox.checkStateChanged.connect(self.main_window.refresh) - self.filter[column]['widget'] = filter_checkbox + self.filter[column][ColumnEntry.COLUMN_WIDGET] = filter_checkbox filter_layout.addWidget(filter_checkbox) filter_layout.addStretch() # self.log.info("get_filter_layout: %s", self.filter) diff --git a/python/kontor-gui/gui/table_model.py b/python/kontor-gui/gui/table_model.py index 59340a9..ae23001 100644 --- a/python/kontor-gui/gui/table_model.py +++ b/python/kontor-gui/gui/table_model.py @@ -1,12 +1,45 @@ from datetime import datetime +from typing import Any from PySide6.QtCore import QAbstractTableModel, QModelIndex -from PySide6.QtGui import Qt +from PySide6.QtGui import Qt, QColor from kontor_schema.database import ColumnEntry from .model_config import KontorModelConfig +def get_display_value(value: Any, column_config: dict, window) -> str: + if isinstance(value, datetime): + return value.strftime("%Y-%m-%d %M:%M:%S") + if column_config[ColumnEntry.COLUMN_TYPE] == 'BOOLEAN': + if value == 1: + return window.tick + else: + return window.cross + if value is None: + return "" + # window.log.info(f"unknown type: {column_config[ColumnEntry.COLUMN_TYPE]} - {type(value)}") + return str(value) + + +def get_edit_value(value, column_config, window): + # window.log.info(f"edit value {value}") + return str(value) + + +def get_decoration_value(value: Any, column_config: dict, window): + if column_config[ColumnEntry.COLUMN_TYPE] == 'BOOLEAN': + if value == 1: + return window.tick + else: + return window.cross + + +def get_background_value(value: Any, column_config: dict, window): + if value is None: + return QColor('lightgrey') + + class KontorTableModel(QAbstractTableModel): def __init__(self, model_config: KontorModelConfig): @@ -46,68 +79,41 @@ class KontorTableModel(QAbstractTableModel): if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: return self._config.header[col][ColumnEntry.COLUMN_LABEL] if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: - return str(col+1) + return str(col + 1) def data(self, index, role=Qt.ItemDataRole.DisplayRole): if self._data is None: return None value = self._data[index.row()][index.column()] - # print('{}:: {}:: {}: {}'.format(index, role, value, type(value))) - if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole: - if isinstance(value, datetime): - return value.strftime("%Y-%m-%d %M:%M:%S") - if isinstance(value, str): - return value - if isinstance(value, bytes): - if value == b'\x01': - return self._config.main_window.tick - else: - return self._config.main_window.cross - if isinstance(value, int): - # print('{}:: {}: {}'.format(index, value, type(value))) - if value == 1: - return self._config.main_window.tick - else: - return self._config.main_window.cross - if isinstance(value, bool): - if value: - return self._config.main_window.tick - else: - return self._config.main_window.cross - return str(value) - if role == Qt.ItemDataRole.DecorationRole: - if isinstance(value, bytes): - if value == b'\x01': - return self._config.main_window.tick - else: - return self._config.main_window.cross - if isinstance(value, int): - if value == 1: - return self._config.main_window.tick - else: - return self._config.main_window.cross - if isinstance(value, bool): - if value: - return self._config.main_window.tick - else: - return self._config.main_window.cross + # print('{}:: {}:: {}:: {}: {}'.format(index, role, self._config.header[index.column()][ColumnEntry.COLUMN_TYPE], value, type(value))) + match role: + case Qt.ItemDataRole.DisplayRole: + return get_display_value(value, self._config.header[index.column()], self._config.main_window) + case Qt.ItemDataRole.EditRole: + return get_edit_value(value, self._config.header[index.column()], self._config.main_window) + case Qt.ItemDataRole.DecorationRole: + return get_decoration_value(value, self._config.header[index.column()], self._config.main_window) + case Qt.ItemDataRole.BackgroundRole: + return get_background_value(value, self._config.header[index.column()], self._config.main_window) def columnCount(self, index=QModelIndex()): # self.log.info("rowCount %s: %d", self, len(self._config.header)) return len(self._config.header) - def setData(self, index, value, role: int) -> bool: - # print(index, role) + def setData(self, index, value, role=Qt.ItemDataRole.EditRole) -> bool: + # self._config.log.info(f"{index}: {role}") if role == Qt.ItemDataRole.EditRole: self._data[index.row()][index.column()] = value - # print(self._data[index.row()][index.column()]) + # self._config.log.info(f"{index.row()}-{index.column()}: {self._data[index.row()][index.column()]}") self.dataChanged.emit(index, index) return True if role == Qt.ItemDataRole.CheckStateRole: - # print("role == Qt.ItemDataRole.CheckStateRole") + print("role == Qt.ItemDataRole.CheckStateRole") checked = value == Qt.CheckState.Checked self._data[index.row()][index.column()] = checked return False def flags(self, index): + if self._config.header[index.column()][ColumnEntry.COLUMN_NAME] == 'id': + return Qt.ItemFlag.ItemIsEnabled return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate diff --git a/python/kontor-schema/kontor_schema/__init__.py b/python/kontor-schema/kontor_schema/__init__.py index 827b402..2f0718b 100644 --- a/python/kontor-schema/kontor_schema/__init__.py +++ b/python/kontor-schema/kontor_schema/__init__.py @@ -7,4 +7,4 @@ from .metadata import MetaDataTable, MetaDataColumn from .tysc import Card, CardSet, Sport, Team, FieldPosition, Rooster, Player, Vendor from .media import MediaFile, MediaArticle, MediaVideo from .base import Base -from .database import KontorDB +from .database import KontorDB, ColumnEntry From 7dc18b10cba676b1533d2dbf0ad4fd29ee0a3553 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Thu, 6 Feb 2025 11:09:09 +0100 Subject: [PATCH 78/91] refactor kontor-schema by moving classes to seprate modules --- .../kontor-cli/kontor/controllers/__init__.py | 7 + .../kontor-cli/kontor/controllers/database.py | 30 +- .../kontor/templates/export_db.jinja2 | 5 - python/kontor-gui/gui/main_window.py | 2 +- python/kontor-gui/gui/model_config.py | 3 +- python/kontor-gui/gui/table_model.py | 2 +- .../kontor-schema/kontor_schema/__init__.py | 374 +---------------- .../kontor-schema/kontor_schema/database.py | 388 ++++++++++++++++++ 8 files changed, 419 insertions(+), 392 deletions(-) delete mode 100644 python/kontor-cli/kontor/templates/export_db.jinja2 create mode 100644 python/kontor-schema/kontor_schema/database.py diff --git a/python/kontor-cli/kontor/controllers/__init__.py b/python/kontor-cli/kontor/controllers/__init__.py index e69de29..3f2fa1c 100644 --- a/python/kontor-cli/kontor/controllers/__init__.py +++ b/python/kontor-cli/kontor/controllers/__init__.py @@ -0,0 +1,7 @@ +from enum import Enum + +class ArgumentData(Enum): + EXPORT_TYPE = 'export_type' + DB_FILE = 'db_file' + DATA_TYPE = 'data_type' + DELETE_FIRST = 'delete_first' diff --git a/python/kontor-cli/kontor/controllers/database.py b/python/kontor-cli/kontor/controllers/database.py index c748f75..b5973d2 100644 --- a/python/kontor-cli/kontor/controllers/database.py +++ b/python/kontor-cli/kontor/controllers/database.py @@ -1,6 +1,8 @@ import mariadb from cement import Controller, ex +from kontor.controllers import ArgumentData + class Database(Controller): class Meta: @@ -24,16 +26,16 @@ class Database(Controller): ) def export(self): data = { - 'db_file': 'data.json', - 'export_type': 'JSON', + ArgumentData.DB_FILE: 'data.json', + ArgumentData.EXPORT_TYPE: 'JSON', } if self.app.pargs.db_file is not None: - data['db_file'] = self.app.pargs.db_file + data[ArgumentData.DB_FILE] = self.app.pargs.db_file db = self.app.kontor_db - self.app.log.info(f"export DB to {data['db_file']} as {data['export_type']}") - results = db.export_db(data['export_type'], data['db_file']) - data['results'] = results - self.app.render(data, 'export_db.jinja2') + self.app.log.info(f"export DB to {data[ArgumentData.DB_FILE]} as {data[ArgumentData.EXPORT_TYPE]}") + results = db.export_db(data[ArgumentData.EXPORT_TYPE], data[ArgumentData.DB_FILE]) + for key, value in results.items(): + self.app.log.info(f"{key}: {value}") @ex( label='import', @@ -51,16 +53,18 @@ class Database(Controller): ) def import_cmd(self): data = { - 'db_file': 'data.json', - 'data_type': 'JSON', - 'delete_first': False, + ArgumentData.DB_FILE: 'data.json', + ArgumentData.DATA_TYPE: 'JSON', + ArgumentData.DELETE_FIRST: False, } if self.app.pargs.db_file is not None: - data['db_file'] = self.app.pargs.db_file + data[ArgumentData.DB_FILE] = self.app.pargs.db_file if self.app.pargs.delete_first is not None: - data['delete_first'] = self.app.pargs.delete_first + data[ArgumentData.DELETE_FIRST] = self.app.pargs.delete_first db = self.app.kontor_db - db.import_db(data['db_file'], data['delete_first']) + if data[ArgumentData.DELETE_FIRST]: + db.delete_entries() + db.import_db(data[ArgumentData.DB_FILE]) @ex( help='check the db schema against MetaDataTable and MetaDataColumn' diff --git a/python/kontor-cli/kontor/templates/export_db.jinja2 b/python/kontor-cli/kontor/templates/export_db.jinja2 deleted file mode 100644 index 678aaa3..0000000 --- a/python/kontor-cli/kontor/templates/export_db.jinja2 +++ /dev/null @@ -1,5 +0,0 @@ - Following tables were exported: - -{% for key, value in results.items() %} -Table {{key}}: {{value}} entries -{% endfor %} diff --git a/python/kontor-gui/gui/main_window.py b/python/kontor-gui/gui/main_window.py index a1921d7..e30727a 100644 --- a/python/kontor-gui/gui/main_window.py +++ b/python/kontor-gui/gui/main_window.py @@ -181,7 +181,7 @@ class MainWindow(QMainWindow): import_dlg = ImportKontorDialog(self) if import_dlg.exec(): print(f"import DB from file {import_dlg.file_name}") - self.kontor_db.import_db(import_dlg.file_name, False) + self.kontor_db.import_db(import_dlg.file_name) else: print("do nothing for import") diff --git a/python/kontor-gui/gui/model_config.py b/python/kontor-gui/gui/model_config.py index 1259fc3..a987c94 100644 --- a/python/kontor-gui/gui/model_config.py +++ b/python/kontor-gui/gui/model_config.py @@ -1,5 +1,6 @@ from PySide6.QtWidgets import QHBoxLayout, QCheckBox, QMdiSubWindow -from kontor_schema import KontorDB, ColumnEntry +from kontor_schema import KontorDB +from kontor_schema.database import ColumnEntry class KontorModelConfig: diff --git a/python/kontor-gui/gui/table_model.py b/python/kontor-gui/gui/table_model.py index 345e336..884b188 100644 --- a/python/kontor-gui/gui/table_model.py +++ b/python/kontor-gui/gui/table_model.py @@ -2,7 +2,7 @@ from datetime import datetime from PySide6.QtCore import QAbstractTableModel, QModelIndex from PySide6.QtGui import Qt -from kontor_schema import ColumnEntry +from kontor_schema.database import ColumnEntry from .model_config import KontorModelConfig diff --git a/python/kontor-schema/kontor_schema/__init__.py b/python/kontor-schema/kontor_schema/__init__.py index 0a12a3b..827b402 100644 --- a/python/kontor-schema/kontor_schema/__init__.py +++ b/python/kontor-schema/kontor_schema/__init__.py @@ -1,378 +1,10 @@ -import json -import uuid -from datetime import datetime -from enum import Enum -from logging import Logger -from pathlib import Path +from enum import Enum, auto -from sqlalchemy import Engine, select -from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import sessionmaker - -from .base import Base, BaseMixin from .admin import User, Token, Role, AuthorizationMatrix, ModuleData, MailAccount, Mail from .bookshelf import Article, Book, Author, BookshelfPublisher, ArticleAuthor, BookAuthor from .comic import Comic, Artist, Publisher, Issue, StoryArc, TradePaperback, Volume, ComicWork, WorkType from .metadata import MetaDataTable, MetaDataColumn from .tysc import Card, CardSet, Sport, Team, FieldPosition, Rooster, Player, Vendor from .media import MediaFile, MediaArticle, MediaVideo - - -class ColumnEntry(Enum): - COLUMN_NAME = "column" - COLUMN_LABEL = 'label' - COLUMN_ORDER = 'order' - COLUMN_REF_COLUMN = 'ref_column' - COLUMN_TYPE = "type" - COLUMN_WIDGET = 'widget' - - -class KontorDB: - - def __init__(self, db_engine: Engine, log: Logger): - self.engine = db_engine - self.registry = {} - self.init_registry() - self.log = log - - def init_registry(self): - self.registry[Card.__tablename__] = Card - self.registry[CardSet.__tablename__] = CardSet - self.registry[Rooster.__tablename__] = Rooster - self.registry[Team.__tablename__] = Team - self.registry[FieldPosition.__tablename__] = FieldPosition - self.registry[Player.__tablename__] = Player - self.registry[Vendor.__tablename__] = Vendor - self.registry[Sport.__tablename__] = Sport - self.registry[Issue.__tablename__] = Issue - self.registry[TradePaperback.__tablename__] = TradePaperback - self.registry[StoryArc.__tablename__] = StoryArc - self.registry[Volume.__tablename__] = Volume - self.registry[ComicWork.__tablename__] = ComicWork - self.registry[Artist.__tablename__] = Artist - self.registry[Comic.__tablename__] = Comic - self.registry[Publisher.__tablename__] = Publisher - self.registry[WorkType.__tablename__] = WorkType - self.registry[ArticleAuthor.__tablename__] = ArticleAuthor - self.registry[BookAuthor.__tablename__] = BookAuthor - self.registry[BookshelfPublisher.__tablename__] = BookshelfPublisher - self.registry[Article.__tablename__] = Article - self.registry[Book.__tablename__] = Book - self.registry[Author.__tablename__] = Author - self.registry[MediaFile.__tablename__] = MediaFile - self.registry[MediaArticle.__tablename__] = MediaArticle - self.registry[MediaVideo.__tablename__] = MediaVideo - self.registry[MetaDataColumn.__tablename__] = MetaDataColumn - self.registry[MetaDataTable.__tablename__] = MetaDataTable - self.registry[AuthorizationMatrix.__tablename__] = AuthorizationMatrix - self.registry[Token.__tablename__] = Token - self.registry[User.__tablename__] = User - self.registry[Role.__tablename__] = Role - self.registry[ModuleData.__tablename__] = ModuleData - self.registry[MailAccount.__tablename__] = MailAccount - self.registry[Mail.__tablename__] = Mail - - def get_table_names(self) -> list: - result = [] - __session__ = sessionmaker(self.engine) - with __session__() as session: - tables = session.scalars(select(MetaDataTable)).all() - result = [table.table_name for table in tables] - return result - - def get_column_meta_data(self, table_name: str, view_only=True) -> dict: - meta_data = {} - order = 0 - __session__ = sessionmaker(self.engine) - with __session__() as session: - if view_only: - for (_, column) in (session.query(MetaDataTable, MetaDataColumn). - filter(MetaDataTable.id == MetaDataColumn.table_id). - filter(MetaDataTable.table_name == table_name). - filter(MetaDataColumn.is_shown == 1).all()): - # self.log.info("get_column_meta_data: %s %s %d", column.column_name, column.column_label, column.column_order) - meta_data[order] = { - ColumnEntry.COLUMN_NAME: column.column_name, - ColumnEntry.COLUMN_LABEL: column.column_label, - ColumnEntry.COLUMN_ORDER: column.column_order, - ColumnEntry.COLUMN_REF_COLUMN: column.ref_column, - ColumnEntry.COLUMN_TYPE: column.column_type - } - order += 1 - else: - for (_, column) in (session.query(MetaDataTable, MetaDataColumn). - filter(MetaDataTable.id == MetaDataColumn.table_id). - filter(MetaDataTable.table_name == table_name).all()): - meta_data[order] = { - ColumnEntry.COLUMN_NAME: column.column_name, - ColumnEntry.COLUMN_ORDER: column.column_order, - ColumnEntry.COLUMN_REF_COLUMN: column.ref_column, - ColumnEntry.COLUMN_TYPE: column.column_type - } - order += 1 - # self.log.info("get_column_meta_data: %s", meta_data) - return meta_data - - def get_columns(self, table_name: str) -> dict: - columns = {} - __session__ = sessionmaker(self.engine) - with __session__() as session: - for (_, column) in (session.query(MetaDataTable, MetaDataColumn). - filter(MetaDataTable.id == MetaDataColumn.table_id). - filter(MetaDataTable.table_name == table_name).all()): - columns[column.column_name] = { - ColumnEntry.COLUMN_ORDER: column.column_order, - ColumnEntry.COLUMN_TYPE: column.column_type - } - return columns - - def get_filters(self, table_name: str) -> dict: - _filter_map = {} - __session__ = sessionmaker(self.engine) - with __session__() as session: - for (_, column) in (session.query(MetaDataTable, MetaDataColumn). - filter(MetaDataTable.id == MetaDataColumn.table_id). - filter(MetaDataTable.table_name == table_name). - filter(MetaDataColumn.show_filter == 1).all()): - _filter_map[column.column_name] = { - ColumnEntry.COLUMN_LABEL: column.filter_label, - ColumnEntry.COLUMN_WIDGET: None - } - return _filter_map - - def data(self, table_name: str, columns: dict, filters: dict) -> list: - data = [] - __session__ = sessionmaker(self.engine) - table = self.registry[table_name] - with __session__() as session: - entries = [] - if len(filters) == 0: - entries = session.scalars(select(table)).all() - else: - entries = session.scalars(select(table).filter_by(**filters)).all() - for entry in entries: - # self.log.info("data: %s", entry) - row = [] - for order in columns.keys(): - column_name = columns[order][ColumnEntry.COLUMN_NAME] - ref_column = columns[order][ColumnEntry.COLUMN_REF_COLUMN] - if str(column_name).endswith("_id"): - ref_table = column_name[:-3] - ref = getattr(entry, ref_table) - value = getattr(ref, ref_column) - row.append(value) - else: - row.append(getattr(entry, column_name)) - data.append(row) - # self.log.info("data: %s", data) - return data - - def export_db(self, export_type: str, export_file_name: str) -> dict: - results = {} - db = {} - export_table_list = self.get_table_names() - for table in export_table_list: - columns = self.get_column_meta_data(table, view_only=False) - if table in self.registry: - model = self.registry[table] - else: - self.log.info(f"table {table} is not registered") - continue - __session__ = sessionmaker(self.engine) - with __session__() as session: - rows = session.query(model).all() - entries = [] - for row in rows: - # print(row) - entry = {} - for order in columns: - # print(columns[order]) - column_name = columns[order][ColumnEntry.COLUMN_NAME] - # print(f"get value {column_name} from {row} of table {table}") - try: - value = getattr(row, column_name) - if isinstance(value, datetime): - entry[column_name] = str(value) - else: - entry[column_name] = value - except AttributeError: - pass - entries.append(entry) - db[table] = entries - results[table] = len(entries) - match export_type: - case "JSON": - json_dump = json.dumps(db, indent=4) - with open(export_file_name, "w") as dump_file: - dump_file.write(json_dump) - case "YAML": - export_file = Path(export_file_name) - case "SQLite": - export_file = Path(export_file_name) - self.log.info("%d tables exported", len(results)) - return results - - def import_db(self, import_file_name: str, delete_first: bool) -> dict: - result = {} - if delete_first: - self.delete_entries() - import_file = Path(import_file_name) - if not import_file.exists(): - self.log.info(f"File {import_file_name} does not exist. Do nothing.") - return result - match import_file.suffix: - case '.json': - print("read json file") - with open(import_file_name, 'r') as json_file: - json_load = json.load(json_file) - for table in json_load: - self.log.info(f"{table}: {len(json_load[table])}") - result[table] = self.import_table(table, json_load[table]) - case '.yml': - print("read yaml file") - case '.yaml': - print("read yaml file") - case '.db': - print("read sqlite file") - return result - - def import_table(self, table_name: str, items:list) -> dict: - result = {} - updated = [] - added = [] - remaining = [] - existing_ids = self.get_ids(table_name) - self.log.info(f"found {len(existing_ids)} existing ids for table {table_name}") - for item in items: - current_id = item['id'] - # print(f"import item: {item}") - found_item = None - __session__ = sessionmaker(self.engine) - with __session__() as session: - found_item = session.get(self.registry[table_name], current_id) - # print(f"found item: {found_item}") - if found_item is not None: - changed = self.update_entry(table_name, current_id, item) - updated.append(item) - if changed: - self.log.info(f"{current_id} has changed") - updated.append(item) - existing_ids.remove(current_id) - else: - try: - self.add_entry(table_name, item) - added.append(item) - except IntegrityError as error: - self.log.info(f"Could not add item, due to: {error.detail}") - if len(existing_ids) > 0: - print(f"remaining items: {existing_ids}") - remaining.extend(existing_ids) - result['updated'] = updated - result['added'] = added - result['remaining'] = remaining - return result - - def get_ids(self, table_name: str) -> list: - existing_ids = [] - __session__ = sessionmaker(self.engine) - with __session__() as session: - items = session.query(self.registry[table_name]).all() - for item in items: - existing_ids.append(getattr(item, 'id')) - return existing_ids - - def add_entry(self, table_name: str, update_item: dict): - self.log.debug(f"add entry to table {table_name} with {update_item}") - __session__ = sessionmaker(self.engine) - with __session__() as session: - add_item = self.registry[table_name]() - for key in update_item.keys(): - update_value = update_item[key] - setattr(add_item, key, update_value) - session.add(add_item) - session.commit() - - def update_entry(self, table_name, current_id, update_item: dict) -> bool: - # self.log.info("update entry to table %s", table_name) - __session__ = sessionmaker(self.engine) - with __session__() as session: - existing_item = session.query(self.registry[table_name]).get(current_id) - changed = False - for key in update_item.keys(): - update_value = update_item[key] - existing_value = getattr(existing_item, key) - if type(existing_value) is not type(update_value): - existing_value = str(existing_value) - if existing_value != update_value: - self.log.info(f"{key} has changed: {existing_value} != {update_value}") - setattr(existing_item, key, update_value) - session.commit() - changed = True - self.log.info(f"update {key} with {update_value}") - return changed - - def add_link(self, link: str) -> dict: - result = {} - __session__ = sessionmaker(self.engine) - with __session__() as session: - media_file = MediaFile() - media_file.id = str(uuid.uuid4()) - media_file.created_date = datetime.now() - media_file.last_modified_date = datetime.now() - media_file.version = 0 - media_file.url = link - media_file.review = 1 - media_file.should_download = 1 - try: - session.add(media_file) - session.commit() - result['added'] = {'url': media_file.url, 'title': media_file.title, 'review': media_file.review, 'download': media_file.should_download} - except IntegrityError as error: - session.rollback() - result['error'] = error.orig - return result - - def update_titles(self) -> dict: - update_list = {} - __session__ = sessionmaker(self.engine) - with __session__() as session: - links = session.query(MediaFile).filter(MediaFile.review == 1).all() - for link in links: - url = link.url - if url is None: - continue - link.update_title() - session.commit() - update_list[link.id] = link.title - return update_list - - def get_download_list(self) -> list: - download_list = [] - __session__ = sessionmaker(self.engine) - with __session__() as session: - links = session.query(MediaFile).filter(MediaFile.should_download == 1).all() - for link in links: - url = link.url - if url is None: - continue - download_list.append(link.id) - return download_list - - def download_file(self, entry_id: str, download_dir = "/data/media", dl_tool = "yt-dlp") -> str: - __session__ = sessionmaker(self.engine) - with __session__() as session: - link = session.query(MediaFile).get(entry_id) - link.download_file(download_dir, dl_tool) - session.commit() - file_name = link.file_name - return file_name - - def delete_entries(self): - for (table_name, table) in self.registry.items(): - # self.log.info("delete entries from table %s", table_name) - __session__ = sessionmaker(self.engine) - with __session__() as session: - items = session.query(table).all() - for item in items: - session.delete(item) - session.commit() +from .base import Base +from .database import KontorDB diff --git a/python/kontor-schema/kontor_schema/database.py b/python/kontor-schema/kontor_schema/database.py new file mode 100644 index 0000000..f722ef3 --- /dev/null +++ b/python/kontor-schema/kontor_schema/database.py @@ -0,0 +1,388 @@ +import json +import uuid +from datetime import datetime +from enum import Enum, auto +from logging import Logger +from pathlib import Path + +from sqlalchemy import Engine, select +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import sessionmaker + +from .tysc import Card, CardSet, Rooster, Team, FieldPosition, Player, Vendor, Sport +from .comic import Issue, TradePaperback, StoryArc, Volume, ComicWork, Artist, Comic, Publisher, WorkType +from .bookshelf import ArticleAuthor, BookAuthor, BookshelfPublisher, Article, Book, Author +from .admin import Mail, MailAccount, ModuleData, Role, User, Token, AuthorizationMatrix +from .metadata import MetaDataTable, MetaDataColumn +from .media import MediaVideo, MediaArticle, MediaFile + + +class ColumnEntry(Enum): + COLUMN_NAME = 'column' + COLUMN_LABEL = 'label' + COLUMN_ORDER = 'order' + COLUMN_REF_COLUMN = 'ref_column' + COLUMN_TYPE = 'type' + COLUMN_WIDGET = 'widget' + + +class StatusType(Enum): + UNKNOWN = auto() + FILE_NAME = auto() + FILE_ID = auto() + DUPLICATE = auto() + CLOUD_LINK = auto() + CLOUD_LINK_ID = auto() + + +class KontorDB: + + def __init__(self, db_engine: Engine, log: Logger): + self.engine = db_engine + self.registry = {} + self.init_registry() + self.log = log + + def init_registry(self): + self.registry[Card.__tablename__] = Card + self.registry[CardSet.__tablename__] = CardSet + self.registry[Rooster.__tablename__] = Rooster + self.registry[Team.__tablename__] = Team + self.registry[FieldPosition.__tablename__] = FieldPosition + self.registry[Player.__tablename__] = Player + self.registry[Vendor.__tablename__] = Vendor + self.registry[Sport.__tablename__] = Sport + self.registry[Issue.__tablename__] = Issue + self.registry[TradePaperback.__tablename__] = TradePaperback + self.registry[StoryArc.__tablename__] = StoryArc + self.registry[Volume.__tablename__] = Volume + self.registry[ComicWork.__tablename__] = ComicWork + self.registry[Artist.__tablename__] = Artist + self.registry[Comic.__tablename__] = Comic + self.registry[Publisher.__tablename__] = Publisher + self.registry[WorkType.__tablename__] = WorkType + self.registry[ArticleAuthor.__tablename__] = ArticleAuthor + self.registry[BookAuthor.__tablename__] = BookAuthor + self.registry[BookshelfPublisher.__tablename__] = BookshelfPublisher + self.registry[Article.__tablename__] = Article + self.registry[Book.__tablename__] = Book + self.registry[Author.__tablename__] = Author + self.registry[MediaFile.__tablename__] = MediaFile + self.registry[MediaArticle.__tablename__] = MediaArticle + self.registry[MediaVideo.__tablename__] = MediaVideo + self.registry[MetaDataColumn.__tablename__] = MetaDataColumn + self.registry[MetaDataTable.__tablename__] = MetaDataTable + self.registry[AuthorizationMatrix.__tablename__] = AuthorizationMatrix + self.registry[Token.__tablename__] = Token + self.registry[User.__tablename__] = User + self.registry[Role.__tablename__] = Role + self.registry[ModuleData.__tablename__] = ModuleData + self.registry[MailAccount.__tablename__] = MailAccount + self.registry[Mail.__tablename__] = Mail + + def get_table_names(self) -> list: + result = [] + __session__ = sessionmaker(self.engine) + with __session__() as session: + tables = session.scalars(select(MetaDataTable)).all() + result = [table.table_name for table in tables] + return result + + def get_column_meta_data(self, table_name: str, view_only=True) -> dict: + meta_data = {} + order = 0 + __session__ = sessionmaker(self.engine) + with __session__() as session: + if view_only: + for (_, column) in (session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name). + filter(MetaDataColumn.is_shown == 1).all()): + # self.log.info("get_column_meta_data: %s %s %d", column.column_name, column.column_label, column.column_order) + meta_data[order] = { + ColumnEntry.COLUMN_NAME: column.column_name, + ColumnEntry.COLUMN_LABEL: column.column_label, + ColumnEntry.COLUMN_ORDER: column.column_order, + ColumnEntry.COLUMN_REF_COLUMN: column.ref_column, + ColumnEntry.COLUMN_TYPE: column.column_type + } + order += 1 + else: + for (_, column) in (session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name).all()): + meta_data[order] = { + ColumnEntry.COLUMN_NAME: column.column_name, + ColumnEntry.COLUMN_ORDER: column.column_order, + ColumnEntry.COLUMN_REF_COLUMN: column.ref_column, + ColumnEntry.COLUMN_TYPE: column.column_type + } + order += 1 + # self.log.info("get_column_meta_data: %s", meta_data) + return meta_data + + def get_columns(self, table_name: str) -> dict: + columns = {} + order = 0 + __session__ = sessionmaker(self.engine) + with __session__() as session: + for (_, column) in (session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name).all()): + columns[column.column_name] = { + ColumnEntry.COLUMN_ORDER: column.column_order, + ColumnEntry.COLUMN_TYPE: column.column_type + } + return columns + + def get_filters(self, table_name: str) -> dict: + _filter_map = {} + __session__ = sessionmaker(self.engine) + with __session__() as session: + for (_, column) in (session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name). + filter(MetaDataColumn.show_filter == 1).all()): + _filter_map[column.column_name] = { + ColumnEntry.COLUMN_LABEL: column.filter_label, + ColumnEntry.COLUMN_WIDGET: None + } + return _filter_map + + def data(self, table_name: str, columns: dict, filters: dict) -> list: + data = [] + __session__ = sessionmaker(self.engine) + table = self.registry[table_name] + with __session__() as session: + entries = [] + if len(filters) == 0: + entries = session.scalars(select(table)).all() + else: + entries = session.scalars(select(table).filter_by(**filters)).all() + for entry in entries: + # self.log.info("data: %s", entry) + row = [] + for order in columns.keys(): + column_name = columns[order][ColumnEntry.COLUMN_NAME] + ref_column = columns[order][ColumnEntry.COLUMN_REF_COLUMN] + if str(column_name).endswith("_id"): + ref_table = column_name[:-3] + ref = getattr(entry, ref_table) + value = getattr(ref, ref_column) + row.append(value) + else: + row.append(getattr(entry, column_name)) + data.append(row) + # self.log.info("data: %s", data) + return data + + def export_db(self, export_type: str, export_file_name: str) -> dict: + results = {} + db = {} + export_table_list = self.get_table_names() + for table in export_table_list: + columns = self.get_column_meta_data(table, view_only=False) + if table in self.registry: + model = self.registry[table] + else: + self.log.info(f"table {table} is not registered") + continue + __session__ = sessionmaker(self.engine) + with __session__() as session: + rows = session.query(model).all() + entries = [] + for row in rows: + # print(row) + entry = {} + for order in columns: + # print(columns[order]) + column_name = columns[order][ColumnEntry.COLUMN_NAME] + # print(f"get value {column_name} from {row} of table {table}") + try: + value = getattr(row, column_name) + if isinstance(value, datetime): + entry[column_name] = str(value) + else: + entry[column_name] = value + except AttributeError: + pass + entries.append(entry) + db[table] = entries + results[table] = len(entries) + match export_type: + case "JSON": + json_dump = json.dumps(db, indent=4) + with open(export_file_name, "w") as dump_file: + dump_file.write(json_dump) + case "YAML": + export_file = Path(export_file_name) + case "SQLite": + export_file = Path(export_file_name) + self.log.info("%d tables exported", len(results)) + return results + + def import_db(self, import_file_name: str) -> dict: + result = {} + import_file = Path(import_file_name) + if not import_file.exists(): + self.log.info(f"File {import_file_name} does not exist. Do nothing.") + return result + match import_file.suffix: + case '.json': + print("read json file") + with open(import_file_name, 'r') as json_file: + json_load = json.load(json_file) + for table in json_load: + self.log.info(f"{table}: {len(json_load[table])}") + result[table] = self.import_table(table, json_load[table]) + case '.yml': + print("read yaml file") + case '.yaml': + print("read yaml file") + case '.db': + print("read sqlite file") + return result + + def import_table(self, table_name: str, items:list) -> dict: + result = {} + updated = [] + added = [] + remaining = [] + existing_ids = self.get_ids(table_name) + self.log.info(f"found {len(existing_ids)} existing ids for table {table_name}") + for item in items: + current_id = item['id'] + # print(f"import item: {item}") + found_item = None + __session__ = sessionmaker(self.engine) + with __session__() as session: + found_item = session.get(self.registry[table_name], current_id) + # print(f"found item: {found_item}") + if found_item is not None: + changed = self.update_entry(table_name, current_id, item) + updated.append(item) + if changed: + self.log.info(f"{current_id} has changed") + updated.append(item) + existing_ids.remove(current_id) + else: + try: + self.add_entry(table_name, item) + added.append(item) + except IntegrityError as error: + self.log.info(f"Could not add item, due to: {error.detail}") + if len(existing_ids) > 0: + print(f"remaining items: {existing_ids}") + remaining.extend(existing_ids) + result['updated'] = updated + result['added'] = added + result['remaining'] = remaining + return result + + def get_ids(self, table_name: str) -> list: + existing_ids = [] + __session__ = sessionmaker(self.engine) + with __session__() as session: + items = session.query(self.registry[table_name]).all() + for item in items: + existing_ids.append(getattr(item, 'id')) + return existing_ids + + def add_entry(self, table_name: str, update_item: dict): + self.log.debug(f"add entry to table {table_name} with {update_item}") + __session__ = sessionmaker(self.engine) + with __session__() as session: + add_item = self.registry[table_name]() + for key in update_item.keys(): + update_value = update_item[key] + setattr(add_item, key, update_value) + session.add(add_item) + session.commit() + + def update_entry(self, table_name, current_id, update_item: dict) -> bool: + # self.log.info("update entry to table %s", table_name) + __session__ = sessionmaker(self.engine) + with __session__() as session: + existing_item = session.query(self.registry[table_name]).get(current_id) + changed = False + for key in update_item.keys(): + update_value = update_item[key] + existing_value = getattr(existing_item, key) + if type(existing_value) is not type(update_value): + existing_value = str(existing_value) + if existing_value != update_value: + self.log.info(f"{key} has changed: {existing_value} != {update_value}") + setattr(existing_item, key, update_value) + session.commit() + changed = True + self.log.info(f"update {key} with {update_value}") + return changed + + def add_link(self, link: str) -> dict: + result = {} + __session__ = sessionmaker(self.engine) + with __session__() as session: + media_file = MediaFile() + media_file.id = str(uuid.uuid4()) + media_file.created_date = datetime.now() + media_file.last_modified_date = datetime.now() + media_file.version = 0 + media_file.url = link + media_file.review = 1 + media_file.should_download = 1 + try: + session.add(media_file) + session.commit() + result['added'] = {'url': media_file.url, 'title': media_file.title, 'review': media_file.review, 'download': media_file.should_download} + except IntegrityError as error: + session.rollback() + result['error'] = error.orig + return result + + def update_titles(self) -> dict: + update_list = {} + __session__ = sessionmaker(self.engine) + with __session__() as session: + links = session.query(MediaFile).filter(MediaFile.review == 1).all() + for link in links: + url = link.url + if url is None: + continue + link.update_title() + session.commit() + update_list[link.id] = link.title + return update_list + + def get_download_list(self) -> list: + download_list = [] + __session__ = sessionmaker(self.engine) + with __session__() as session: + links = session.query(MediaFile).filter(MediaFile.should_download == 1).all() + for link in links: + url = link.url + if url is None: + continue + download_list.append(link.id) + return download_list + + def download_file(self, entry_id: str, download_dir = "/data/media", dl_tool = "yt-dlp") -> str: + __session__ = sessionmaker(self.engine) + with __session__() as session: + link = session.query(MediaFile).get(entry_id) + link.download_file(download_dir, dl_tool) + session.commit() + file_name = link.file_name + return file_name + + def delete_entries(self): + for (table_name, table) in self.registry.items(): + # self.log.info("delete entries from table %s", table_name) + __session__ = sessionmaker(self.engine) + with __session__() as session: + items = session.query(table).all() + for item in items: + session.delete(item) + session.commit() + + def check_files(self): + pass From ed0e1085997696572a85128dae346ce5a0c6e694 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Thu, 6 Feb 2025 16:33:15 +0100 Subject: [PATCH 79/91] refactor project by using enums for recurring strings --- python/kontor-gui/gui/comic_window.py | 7 +- python/kontor-gui/gui/media_window.py | 4 +- python/kontor-gui/gui/meta_data_window.py | 8 +- python/kontor-gui/gui/model_config.py | 3 +- python/kontor-gui/gui/table_model.py | 101 +++++++++--------- .../kontor-schema/kontor_schema/__init__.py | 2 +- 6 files changed, 62 insertions(+), 63 deletions(-) diff --git a/python/kontor-gui/gui/comic_window.py b/python/kontor-gui/gui/comic_window.py index bf8b8d2..c4a9064 100644 --- a/python/kontor-gui/gui/comic_window.py +++ b/python/kontor-gui/gui/comic_window.py @@ -1,5 +1,6 @@ from PySide6.QtCore import Signal, QSortFilterProxyModel -from PySide6.QtWidgets import QWidget, QVBoxLayout, QTabWidget, QTableView, QMdiSubWindow, QHeaderView +from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QTabWidget, QMenu, QTableView, QMdiSubWindow, \ + QHeaderView from gui.model_config import KontorModelConfig from gui.table_model import KontorTableModel @@ -53,11 +54,11 @@ class ComicWindow(QMdiSubWindow): self.data_views.append(model) data_tab.setLayout(layout) table_view = QTableView() + header = table_view.horizontalHeader() + header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) proxy_model = QSortFilterProxyModel() proxy_model.setSourceModel(model) table_view.setSortingEnabled(True) - header = table_view.horizontalHeader() - header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) table_view.setModel(proxy_model) layout.addLayout(table_config.get_filter_layout()) layout.addWidget(table_view) diff --git a/python/kontor-gui/gui/media_window.py b/python/kontor-gui/gui/media_window.py index 132a21e..accbf43 100644 --- a/python/kontor-gui/gui/media_window.py +++ b/python/kontor-gui/gui/media_window.py @@ -57,11 +57,9 @@ class MediaWindow(QMdiSubWindow): proxy_model = QSortFilterProxyModel() proxy_model.setSourceModel(model) table_view.setSortingEnabled(True) - # header = table_view.horizontalHeader() - # header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) - #table_view.setModel(model) table_view.setModel(proxy_model) layout.addLayout(table_config.get_filter_layout()) layout.addWidget(table_view) model.refresh() + table_view.resizeColumnToContents(0) return data_tab diff --git a/python/kontor-gui/gui/meta_data_window.py b/python/kontor-gui/gui/meta_data_window.py index 49c4d73..aeaf972 100644 --- a/python/kontor-gui/gui/meta_data_window.py +++ b/python/kontor-gui/gui/meta_data_window.py @@ -57,11 +57,11 @@ class MetaDataWindow(QMdiSubWindow): # proxy_model = QSortFilterProxyModel() # proxy_model.setSourceModel(model) table_view.setSortingEnabled(True) - header = table_view.horizontalHeader() - header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) - # table_view.setModel(proxy_model) - table_view.setModel(model) + # header = table_view.horizontalHeader() + # header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + table_view.setModel(proxy_model) layout.addLayout(table_config.get_filter_layout()) layout.addWidget(table_view) model.refresh() + table_view.resizeColumnToContents(0) return data_tab diff --git a/python/kontor-gui/gui/model_config.py b/python/kontor-gui/gui/model_config.py index a987c94..1259fc3 100644 --- a/python/kontor-gui/gui/model_config.py +++ b/python/kontor-gui/gui/model_config.py @@ -1,6 +1,5 @@ from PySide6.QtWidgets import QHBoxLayout, QCheckBox, QMdiSubWindow -from kontor_schema import KontorDB -from kontor_schema.database import ColumnEntry +from kontor_schema import KontorDB, ColumnEntry class KontorModelConfig: diff --git a/python/kontor-gui/gui/table_model.py b/python/kontor-gui/gui/table_model.py index 884b188..24290c1 100644 --- a/python/kontor-gui/gui/table_model.py +++ b/python/kontor-gui/gui/table_model.py @@ -1,12 +1,45 @@ from datetime import datetime +from typing import Any from PySide6.QtCore import QAbstractTableModel, QModelIndex -from PySide6.QtGui import Qt +from PySide6.QtGui import Qt, QColor from kontor_schema.database import ColumnEntry from .model_config import KontorModelConfig +def get_display_value(value: Any, column_config: dict, window) -> str: + if isinstance(value, datetime): + return value.strftime("%Y-%m-%d %M:%M:%S") + if column_config[ColumnEntry.COLUMN_TYPE] == 'BOOLEAN': + if value == 1: + return window.tick + else: + return window.cross + if value is None: + return "" + # window.log.info(f"unknown type: {column_config[ColumnEntry.COLUMN_TYPE]} - {type(value)}") + return str(value) + + +def get_edit_value(value, column_config, window): + # window.log.info(f"edit value {value}") + return str(value) + + +def get_decoration_value(value: Any, column_config: dict, window): + if column_config[ColumnEntry.COLUMN_TYPE] == 'BOOLEAN': + if value == 1: + return window.tick + else: + return window.cross + + +def get_background_value(value: Any, column_config: dict, window): + if value is None: + return QColor('lightgrey') + + class KontorTableModel(QAbstractTableModel): def __init__(self, model_config: KontorModelConfig): @@ -47,73 +80,41 @@ class KontorTableModel(QAbstractTableModel): if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: return self._config.header[col][ColumnEntry.COLUMN_LABEL] if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: - return str(col+1) + return str(col + 1) def data(self, index, role=Qt.ItemDataRole.DisplayRole): if self._data is None: return None value = self._data[index.row()][index.column()] - # print('{}:: {}:: {}: {}'.format(index, role, value, type(value))) - row = index.row() - column = index.column() - column_type = self._config.header[column][ColumnEntry.COLUMN_TYPE] - # self.log.info(f"{row}-{column}: {column_type}") - if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole: - if column_type == "BOOLEAN": - if isinstance(value, bytes): - if value == b'\x01': - return self._config.main_window.tick - else: - return self._config.main_window.cross - if isinstance(value, int): - # print('{}:: {}: {}'.format(index, value, type(value))) - if value == 1: - return self._config.main_window.tick - else: - return self._config.main_window.cross - if isinstance(value, bool): - if value: - return self._config.main_window.tick - else: - return self._config.main_window.cross - if isinstance(value, datetime): - return value.strftime("%Y-%m-%d %M:%M:%S") - if isinstance(value, str): - return value - return str(value) - if role == Qt.ItemDataRole.DecorationRole: - if isinstance(value, bytes): - if value == b'\x01': - return self._config.main_window.tick - else: - return self._config.main_window.cross - if isinstance(value, int): - if value == 1: - return self._config.main_window.tick - else: - return self._config.main_window.cross - if isinstance(value, bool): - if value: - return self._config.main_window.tick - else: - return self._config.main_window.cross + # print('{}:: {}:: {}:: {}: {}'.format(index, role, self._config.header[index.column()][ColumnEntry.COLUMN_TYPE], value, type(value))) + match role: + case Qt.ItemDataRole.DisplayRole: + return get_display_value(value, self._config.header[index.column()], self._config.main_window) + case Qt.ItemDataRole.EditRole: + return get_edit_value(value, self._config.header[index.column()], self._config.main_window) + case Qt.ItemDataRole.DecorationRole: + return get_decoration_value(value, self._config.header[index.column()], self._config.main_window) + case Qt.ItemDataRole.BackgroundRole: + return get_background_value(value, self._config.header[index.column()], self._config.main_window) def columnCount(self, index=QModelIndex()): # self.log.info("rowCount %s: %d", self, len(self._config.header)) return len(self._config.header) - def setData(self, index, value, role: int) -> bool: - # print(index, role) + def setData(self, index, value, role=Qt.ItemDataRole.EditRole) -> bool: + # self._config.log.info(f"{index}: {role}") if role == Qt.ItemDataRole.EditRole: self._data[index.row()][index.column()] = value - # print(self._data[index.row()][index.column()]) + # self._config.log.info(f"{index.row()}-{index.column()}: {self._data[index.row()][index.column()]}") self.dataChanged.emit(index, index) return True if role == Qt.ItemDataRole.CheckStateRole: - # print("role == Qt.ItemDataRole.CheckStateRole") + print("role == Qt.ItemDataRole.CheckStateRole") checked = value == Qt.CheckState.Checked self._data[index.row()][index.column()] = checked return False def flags(self, index): + if self._config.header[index.column()][ColumnEntry.COLUMN_NAME] == 'id': + return Qt.ItemFlag.ItemIsEnabled return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate diff --git a/python/kontor-schema/kontor_schema/__init__.py b/python/kontor-schema/kontor_schema/__init__.py index 827b402..2f0718b 100644 --- a/python/kontor-schema/kontor_schema/__init__.py +++ b/python/kontor-schema/kontor_schema/__init__.py @@ -7,4 +7,4 @@ from .metadata import MetaDataTable, MetaDataColumn from .tysc import Card, CardSet, Sport, Team, FieldPosition, Rooster, Player, Vendor from .media import MediaFile, MediaArticle, MediaVideo from .base import Base -from .database import KontorDB +from .database import KontorDB, ColumnEntry From f5fb743503948227b6ceee20be042032f54f0810 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sat, 8 Feb 2025 20:22:21 +0100 Subject: [PATCH 80/91] refactor kontor-schema --- python/kontor-gui/gui/meta_data_window.py | 6 +- .../kontor-schema/kontor_schema/database.py | 71 ++++++++++--------- python/kontor-schema/kontor_schema/media.py | 2 +- 3 files changed, 40 insertions(+), 39 deletions(-) diff --git a/python/kontor-gui/gui/meta_data_window.py b/python/kontor-gui/gui/meta_data_window.py index aeaf972..5c4af30 100644 --- a/python/kontor-gui/gui/meta_data_window.py +++ b/python/kontor-gui/gui/meta_data_window.py @@ -54,11 +54,11 @@ class MetaDataWindow(QMdiSubWindow): self.data_views.append(model) data_tab.setLayout(layout) table_view = QTableView() - # proxy_model = QSortFilterProxyModel() - # proxy_model.setSourceModel(model) - table_view.setSortingEnabled(True) # header = table_view.horizontalHeader() # header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + proxy_model = QSortFilterProxyModel() + proxy_model.setSourceModel(model) + table_view.setSortingEnabled(True) table_view.setModel(proxy_model) layout.addLayout(table_config.get_filter_layout()) layout.addWidget(table_view) diff --git a/python/kontor-schema/kontor_schema/database.py b/python/kontor-schema/kontor_schema/database.py index f722ef3..c4607f6 100644 --- a/python/kontor-schema/kontor_schema/database.py +++ b/python/kontor-schema/kontor_schema/database.py @@ -88,47 +88,47 @@ class KontorDB: result = [table.table_name for table in tables] return result + def get_table_by_name(self, table_name: str) -> dict: + result = {} + __session__ = sessionmaker(self.engine) + _filter = {'table_name': table_name} + with __session__() as session: + table = session.query(MetaDataTable).filter_by(**_filter).one() + result['id'] = table.id + result['table_name'] = table.table_name + return result + def get_column_meta_data(self, table_name: str, view_only=True) -> dict: meta_data = {} order = 0 __session__ = sessionmaker(self.engine) + columns = list() + table_info = self.get_table_by_name(table_name) + _filters = {'table_id': table_info['id']} + if view_only: + _filters['is_shown'] = True with __session__() as session: - if view_only: - for (_, column) in (session.query(MetaDataTable, MetaDataColumn). - filter(MetaDataTable.id == MetaDataColumn.table_id). - filter(MetaDataTable.table_name == table_name). - filter(MetaDataColumn.is_shown == 1).all()): - # self.log.info("get_column_meta_data: %s %s %d", column.column_name, column.column_label, column.column_order) - meta_data[order] = { - ColumnEntry.COLUMN_NAME: column.column_name, - ColumnEntry.COLUMN_LABEL: column.column_label, - ColumnEntry.COLUMN_ORDER: column.column_order, - ColumnEntry.COLUMN_REF_COLUMN: column.ref_column, - ColumnEntry.COLUMN_TYPE: column.column_type - } - order += 1 - else: - for (_, column) in (session.query(MetaDataTable, MetaDataColumn). - filter(MetaDataTable.id == MetaDataColumn.table_id). - filter(MetaDataTable.table_name == table_name).all()): - meta_data[order] = { - ColumnEntry.COLUMN_NAME: column.column_name, - ColumnEntry.COLUMN_ORDER: column.column_order, - ColumnEntry.COLUMN_REF_COLUMN: column.ref_column, - ColumnEntry.COLUMN_TYPE: column.column_type - } - order += 1 - # self.log.info("get_column_meta_data: %s", meta_data) + columns = session.query(MetaDataColumn).filter_by(**_filters).all() + for column in columns: + # self.log.info("get_column_meta_data: %s %s %d", column.column_name, column.column_label, column.column_order) + meta_data[order] = { + ColumnEntry.COLUMN_NAME: column.column_name, + ColumnEntry.COLUMN_LABEL: column.column_label, + ColumnEntry.COLUMN_ORDER: column.column_order, + ColumnEntry.COLUMN_REF_COLUMN: column.ref_column, + ColumnEntry.COLUMN_TYPE: column.column_type + } + order += 1 return meta_data def get_columns(self, table_name: str) -> dict: columns = {} order = 0 __session__ = sessionmaker(self.engine) + table_info = self.get_table_by_name(table_name) + _filters = {'table_id': table_info['id']} with __session__() as session: - for (_, column) in (session.query(MetaDataTable, MetaDataColumn). - filter(MetaDataTable.id == MetaDataColumn.table_id). - filter(MetaDataTable.table_name == table_name).all()): + for column in session.query(MetaDataColumn).filter_by(**_filters).all(): columns[column.column_name] = { ColumnEntry.COLUMN_ORDER: column.column_order, ColumnEntry.COLUMN_TYPE: column.column_type @@ -138,11 +138,10 @@ class KontorDB: def get_filters(self, table_name: str) -> dict: _filter_map = {} __session__ = sessionmaker(self.engine) + table_info = self.get_table_by_name(table_name) + _filters = {'table_id': table_info['id'], 'show_filter': True} with __session__() as session: - for (_, column) in (session.query(MetaDataTable, MetaDataColumn). - filter(MetaDataTable.id == MetaDataColumn.table_id). - filter(MetaDataTable.table_name == table_name). - filter(MetaDataColumn.show_filter == 1).all()): + for column in session.query(MetaDataColumn).filter_by(**_filters).all(): _filter_map[column.column_name] = { ColumnEntry.COLUMN_LABEL: column.filter_label, ColumnEntry.COLUMN_WIDGET: None @@ -342,8 +341,9 @@ class KontorDB: def update_titles(self) -> dict: update_list = {} __session__ = sessionmaker(self.engine) + _filter = { 'review': True} with __session__() as session: - links = session.query(MediaFile).filter(MediaFile.review == 1).all() + links = session.query(MediaFile).filter_by(**_filter).all() for link in links: url = link.url if url is None: @@ -356,8 +356,9 @@ class KontorDB: def get_download_list(self) -> list: download_list = [] __session__ = sessionmaker(self.engine) + _filter = { 'should_download': True} with __session__() as session: - links = session.query(MediaFile).filter(MediaFile.should_download == 1).all() + links = session.query(MediaFile).filter_by(**_filter).all() for link in links: url = link.url if url is None: diff --git a/python/kontor-schema/kontor_schema/media.py b/python/kontor-schema/kontor_schema/media.py index 10338b4..5998c0a 100644 --- a/python/kontor-schema/kontor_schema/media.py +++ b/python/kontor-schema/kontor_schema/media.py @@ -20,7 +20,7 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin): def __str__(self): return f'{self.title}({self.id})' - def update_title(self): + def update_title(self) -> None: print(f"update title for {self.url}") try: r = requests.get(self.url) From 6d54c7f3150414373f8820f1ed63f97fd2fb9b7b Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 10 Feb 2025 00:27:23 +0100 Subject: [PATCH 81/91] add MediaActor --- .../kontor/comics/views/ArtistView.java | 8 +- .../thpeetz/kontor/media/MediaConstants.java | 4 + .../thpeetz/kontor/media/data/MediaActor.java | 28 ++++ .../kontor/media/data/MediaActorFile.java | 30 +++++ .../media/data/MediaActorFileRepository.java | 6 + .../media/data/MediaActorRepository.java | 17 +++ .../thpeetz/kontor/media/data/MediaFile.java | 9 +- .../media/services/MediaFileService.java | 41 ++++-- .../kontor/media/views/MediaActorForm.java | 116 ++++++++++++++++ .../kontor/media/views/MediaActorView.java | 125 ++++++++++++++++++ 10 files changed, 369 insertions(+), 15 deletions(-) create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActor.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorFile.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorFileRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorRepository.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorView.java diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java index f0d294d..f264aa9 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java @@ -25,10 +25,10 @@ import jakarta.annotation.security.PermitAll; @PageTitle("Artist | Comics | Kontor") public class ArtistView extends VerticalLayout { - Grid grid = new Grid<>(Artist.class); - TextField filterText = new TextField(); - ArtistForm form; - ComicService service; + Grid grid = new Grid<>(Artist.class); + TextField filterText = new TextField(); + ArtistForm form; + ComicService service; public ArtistView(ComicService service) { this.service = service; diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/MediaConstants.java b/springboot/src/main/java/de/thpeetz/kontor/media/MediaConstants.java index 866f7b3..d51ba8a 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/media/MediaConstants.java +++ b/springboot/src/main/java/de/thpeetz/kontor/media/MediaConstants.java @@ -2,6 +2,7 @@ package de.thpeetz.kontor.media; import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.sidenav.SideNavItem; +import de.thpeetz.kontor.media.views.MediaActorView; import de.thpeetz.kontor.media.views.MediaArticleView; import de.thpeetz.kontor.media.views.MediaFileView; import de.thpeetz.kontor.media.views.MediaVideoView; @@ -15,9 +16,11 @@ public class MediaConstants { public static final String MEDIAVIDEO_ROUTE = "media/mediavideo"; public static final String MEDIAARTICLE_ROUTE = "media/mediaarticle"; public static final String MEDIA_ROLE = "ROLE_MEDIA"; + public static final String MEDIAACTOR_ROUTE = "media/mediaactor"; private static final String MEDIAFILE = "Media Files"; private static final String MEDIAVIDEO = "Media Videos"; private static final String MEDIAARTICLE = "Media Article"; + private static final String MEDIAACTOR = "Media Actor"; public static SideNavItem getMediaNavigation(ArrayList roles) { SideNavItem media = new SideNavItem(MEDIA, MEDIAFILE_ROUTE, VaadinIcon.VIMEO.create()); @@ -25,6 +28,7 @@ public class MediaConstants { media.addItem(new SideNavItem(MEDIAARTICLE, MediaArticleView.class)); if (roles.contains(MEDIA_ROLE)) { media.addItem(new SideNavItem(MEDIAFILE, MediaFileView.class)); + media.addItem(new SideNavItem(MEDIAACTOR, MediaActorView.class)); } return media; } diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActor.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActor.java new file mode 100644 index 0000000..25cc196 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActor.java @@ -0,0 +1,28 @@ +package de.thpeetz.kontor.media.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Slf4j +@Entity +public class MediaActor extends AbstractEntity { + + @NotEmpty + @Column(unique = true) + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "media_actor", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + List mediaActorFiles; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorFile.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorFile.java new file mode 100644 index 0000000..7d8c511 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorFile.java @@ -0,0 +1,30 @@ +package de.thpeetz.kontor.media.data; + +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +@Table(indexes = {@Index(columnList = "media_file_id, media_actor_id") }, + uniqueConstraints = @UniqueConstraint(columnNames = {"media_file_id", "media_actor_id" }) +) +public class MediaActorFile extends AbstractEntity { + + @ManyToOne + @JoinColumn(name = "media_file_id") + @NotNull + private MediaFile media_file; + + @ManyToOne + @JoinColumn(name = "media_actor_id") + @NotNull + private MediaActor media_actor; +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorFileRepository.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorFileRepository.java new file mode 100644 index 0000000..5f364b7 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorFileRepository.java @@ -0,0 +1,6 @@ +package de.thpeetz.kontor.media.data; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MediaActorFileRepository extends JpaRepository { +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorRepository.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorRepository.java new file mode 100644 index 0000000..973e00a --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActorRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.media.data; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface MediaActorRepository extends JpaRepository { + @Query("select m from MediaActor m " + + "where lower(m.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + List findByNameIgnoreCase(String name); + + MediaActor findByName(String name); +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java index fa7b934..39be1c2 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java @@ -2,14 +2,14 @@ package de.thpeetz.kontor.media.data; import de.thpeetz.kontor.common.data.AbstractEntity; import jakarta.annotation.Nullable; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import jakarta.persistence.UniqueConstraint; +import jakarta.persistence.*; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import java.util.List; + @Slf4j @Getter @Setter @@ -37,4 +37,7 @@ public class MediaFile extends AbstractEntity { @Nullable private String path; + @OneToMany(fetch = FetchType.EAGER, mappedBy = "media_file", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + List mediaActorFiles; } diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java b/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java index ffb0a24..aa36066 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java +++ b/springboot/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java @@ -1,7 +1,6 @@ package de.thpeetz.kontor.media.services; -import de.thpeetz.kontor.media.data.MediaFile; -import de.thpeetz.kontor.media.data.MediaFileRepository; +import de.thpeetz.kontor.media.data.*; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -13,19 +12,22 @@ public class MediaFileService { private final MediaFileRepository mediaFileRepository; - public MediaFileService(MediaFileRepository mediaFileRepository) { + private final MediaActorRepository mediaActorRepository; + + public MediaFileService(MediaFileRepository mediaFileRepository, MediaActorRepository mediaActorRepository) { this.mediaFileRepository = mediaFileRepository; + this.mediaActorRepository = mediaActorRepository; } public List findAllMediaFiles(String stringFilter) { + List results; if (stringFilter == null || stringFilter.isEmpty()) { - log.debug("Found " + mediaFileRepository.count()+ " entries"); - return mediaFileRepository.findAll(); + results = mediaFileRepository.findAll(); } else { - List results = mediaFileRepository.search(stringFilter); - log.debug("Found " + results.size() + " entries"); - return results; + results = mediaFileRepository.search(stringFilter); } + log.debug("Found " + results.size() + " entries"); + return results; } public void saveMediaFile(MediaFile mediaFile) { @@ -39,4 +41,27 @@ public class MediaFileService { public void deleteMediaFile(MediaFile mediaFile) { mediaFileRepository.delete(mediaFile); } + + public List findAllMediaActors(String stringFilter) { + List results; + if (stringFilter == null || stringFilter.isEmpty()) { + results = mediaActorRepository.findAll(); + } else { + results = mediaActorRepository.search(stringFilter); + } + log.debug("Found " + results.size() + " entries"); + return results; + } + + public void saveMediaActor(MediaActor mediaActor) { + if (mediaActor == null) { + log.warn("MediaActor is null. Are you sure you have connected your form to the application?"); + return; + } + mediaActorRepository.save(mediaActor); + } + + public void deleteMediaActor(MediaActor mediaActor) { + mediaActorRepository.delete(mediaActor); + } } diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorForm.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorForm.java new file mode 100644 index 0000000..e7cab60 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorForm.java @@ -0,0 +1,116 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.media.data.MediaActor; +import de.thpeetz.kontor.media.data.MediaActorFile; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class MediaActorForm extends FormLayout { + + TextField name = new TextField("Name"); + // Grid mediaActorFiles = new Grid<>(MediaActorFile.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(MediaActor.class); + + public MediaActorForm() { + addClassName("media-actor-form"); + binder.bindInstanceFields(this); + + // mediaActorFiles.setColumns("media_file.title"); + // mediaActorFiles.getColumnByKey("mediaFile.title").setHeader("MediaFile"); + // mediaActorFiles.getColumns().forEach(col -> col.setAutoWidth(true)); + // add(name, mediaActorFiles, createButtonsLayout()); + add(name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new MediaActorForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new MediaActorForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new MediaActorForm.SaveEvent(this, binder.getBean())); + } + } + + public void setMediaActor(MediaActor mediaActor) { + binder.setBean(mediaActor); + } + + public void setMediaActorFiles(List mediaActorFiles) { + log.info("Setting comic works: {}", mediaActorFiles); + // this.mediaActorFiles.setItems(mediaActorFiles); + } + + public abstract static class MediaActorFormEvent extends ComponentEvent { + private MediaActor mediaActor; + + protected MediaActorFormEvent(MediaActorForm source, MediaActor mediaActor) { + super(source, false); + this.mediaActor = mediaActor; + } + + public MediaActor getMediaActor() { + return mediaActor; + } + } + + public static class SaveEvent extends MediaActorForm.MediaActorFormEvent { + SaveEvent(MediaActorForm source, MediaActor mediaActor) { + super(source, mediaActor); + } + } + + public static class DeleteEvent extends MediaActorForm.MediaActorFormEvent { + DeleteEvent(MediaActorForm source, MediaActor mediaActor) { + super(source, mediaActor); + } + } + + public static class CloseEvent extends MediaActorForm.MediaActorFormEvent { + CloseEvent(MediaActorForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(MediaActorForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(MediaActorForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(MediaActorForm.CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorView.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorView.java new file mode 100644 index 0000000..40727cb --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorView.java @@ -0,0 +1,125 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.views.ArtistForm; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.media.MediaConstants; +import de.thpeetz.kontor.media.data.MediaActor; +import de.thpeetz.kontor.media.services.MediaFileService; +import jakarta.annotation.security.PermitAll; +import lombok.Getter; +import org.springframework.context.annotation.Scope; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = MediaConstants.MEDIAACTOR_ROUTE, layout = MainLayout.class) +@PageTitle("Actor | Media | Kontor") +public class MediaActorView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(MediaActor.class); + TextField filterText = new TextField(); + @Getter + MediaActorForm form; + MediaFileService service; + + public MediaActorView(MediaFileService service) { + this.service = service; + addClassName("media-actor-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("artist-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editMediaActor(event.getValue())); + } + + private void configureForm() { + form = new MediaActorForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveMediaActor); + form.addDeleteListener(this::deleteMediaActor); + form.addCloseListener(e -> closeEditor()); + } + + private void saveMediaActor(MediaActorForm.SaveEvent event) { + service.saveMediaActor(event.getMediaActor()); + updateList(); + closeEditor(); + } + + private void deleteMediaActor(MediaActorForm.DeleteEvent event) { + service.deleteMediaActor(event.getMediaActor()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addMediaActorButton = new Button("Add actor"); + addMediaActorButton.addClickListener(click -> addMediaActor()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addMediaActorButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editMediaActor(MediaActor mediaActor) { + if (mediaActor == null) { + closeEditor(); + } else { + form.setMediaActor(mediaActor); + form.setMediaActorFiles(mediaActor.getMediaActorFiles()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setMediaActor(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addMediaActor() { + grid.asSingleSelect().clear(); + editMediaActor(new MediaActor()); + } + + public void updateList() { + grid.setItems(service.findAllMediaActors(filterText.getValue())); + } +} From 8ffd421c1b20a453f351cfac7b6197dacb95e925 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 10 Feb 2025 12:25:39 +0100 Subject: [PATCH 82/91] add views for MediaActor and adapt MediaFile --- .../thpeetz/kontor/admin/AdminConstants.java | 3 + .../kontor/admin/SetupModuleAdmin.java | 14 +++ .../thpeetz/kontor/media/MediaConstants.java | 2 + .../kontor/media/data/MediaActorFile.java | 4 + .../media/services/MediaFileService.java | 27 ++++- .../media/views/MediaActorFileForm.java | 106 ++++++++++++++++ .../media/views/MediaActorFileView.java | 114 ++++++++++++++++++ .../kontor/media/views/MediaActorForm.java | 17 +-- .../kontor/media/views/MediaActorView.java | 4 +- .../kontor/media/views/MediaFileForm.java | 17 ++- .../kontor/media/views/MediaFileView.java | 8 ++ 11 files changed, 302 insertions(+), 14 deletions(-) create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorFileForm.java create mode 100644 springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorFileView.java diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/AdminConstants.java b/springboot/src/main/java/de/thpeetz/kontor/admin/AdminConstants.java index 63f11b1..ce0e33b 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/AdminConstants.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/AdminConstants.java @@ -6,6 +6,8 @@ import com.vaadin.flow.router.RouterLink; import de.thpeetz.kontor.admin.views.*; import de.thpeetz.kontor.comics.ComicConstants; import de.thpeetz.kontor.comics.views.ComicWorkView; +import de.thpeetz.kontor.media.MediaConstants; +import de.thpeetz.kontor.media.views.MediaActorFileView; public class AdminConstants { @@ -44,6 +46,7 @@ public class AdminConstants { administration.addItem(new SideNavItem(ROLE, RoleView.class)); SideNavItem data = new SideNavItem(DATA, AUTHORIZATION_ROUTE, VaadinIcon.DATABASE.create()); data.addItem(new SideNavItem(ComicConstants.COMICWORK, ComicWorkView.class)); + data.addItem(new SideNavItem(MediaConstants.MEDIAACTORFILE, MediaActorFileView.class)); data.addItem(new SideNavItem(AUTHORIZATION, AuthorizationView.class)); data.addItem(new SideNavItem("Data Import", ModuleDataView.class)); data.addItem(new SideNavItem("Meta Data", MetaDataView.class)); diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java index 5350f69..a819bb2 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java @@ -1,5 +1,6 @@ package de.thpeetz.kontor.admin; +import com.vaadin.flow.component.page.Meta; import de.thpeetz.kontor.admin.data.*; import de.thpeetz.kontor.admin.services.AdminService; import de.thpeetz.kontor.admin.services.MetaDataService; @@ -159,6 +160,19 @@ public class SetupModuleAdmin implements ApplicationListener findAllMediaFiles(String stringFilter) { @@ -64,4 +65,26 @@ public class MediaFileService { public void deleteMediaActor(MediaActor mediaActor) { mediaActorRepository.delete(mediaActor); } + + public List findAllMediaActorFiles() { + return mediaActorFileRepository.findAll(); + } + + public void saveMediaActorFile(MediaActorFile mediaActorFile) { + if (mediaActorFile == null){ + log.warn("MediaActorFile is null. Are you sure you have connected your form to the application?"); + return; + } + mediaActorFileRepository.save(mediaActorFile); + } + + public void deleteMediaActorFile(MediaActorFile mediaActorFile) { + MediaFile mediaFile = mediaActorFile.getMedia_file(); + mediaFile.getMediaActorFiles().remove(mediaActorFile); + mediaFileRepository.save(mediaFile); + MediaActor mediaActor = mediaActorFile.getMedia_actor(); + mediaActor.getMediaActorFiles().remove(mediaActorFile); + mediaActorRepository.save(mediaActor); + mediaActorFileRepository.delete(mediaActorFile); + } } diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorFileForm.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorFileForm.java new file mode 100644 index 0000000..ac3acb1 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorFileForm.java @@ -0,0 +1,106 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.media.data.MediaActor; +import de.thpeetz.kontor.media.data.MediaActorFile; +import de.thpeetz.kontor.media.data.MediaFile; +import lombok.Getter; + +import java.util.List; + +public class MediaActorFileForm extends FormLayout { + ComboBox mediaFile = new ComboBox<>("Media File"); + ComboBox mediaActor = new ComboBox<>("Actor"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(MediaActorFile.class); + + public MediaActorFileForm(List mediaFiles, List actors) { + addClassName("mediaactorfile-form"); + binder.bindInstanceFields(this); + + mediaFile.setItems(mediaFiles); + mediaFile.setItemLabelGenerator(MediaFile::getTitle); + mediaActor.setItems(actors); + mediaActor.setItemLabelGenerator(MediaActor::getName); + add(mediaFile, mediaActor, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new MediaActorFileForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new MediaActorFileForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new MediaActorFileForm.SaveEvent(this, binder.getBean())); + } + } + + public void setMediaActorFile(MediaActorFile mediaActorFile) { + binder.setBean(mediaActorFile); + } + + public abstract static class MediaActorFileFormEvent extends ComponentEvent { + @Getter + private MediaActorFile mediaActorFile; + + protected MediaActorFileFormEvent(MediaActorFileForm source, MediaActorFile mediaActorFile) { + super(source, false); + this.mediaActorFile = mediaActorFile; + } + } + + public static class SaveEvent extends MediaActorFileForm.MediaActorFileFormEvent { + SaveEvent(MediaActorFileForm source, MediaActorFile mediaActorFile) { + super(source, mediaActorFile); + } + } + + public static class DeleteEvent extends MediaActorFileForm.MediaActorFileFormEvent { + DeleteEvent(MediaActorFileForm source, MediaActorFile mediaActorFile) { + super(source, mediaActorFile); + } + } + + public static class CloseEvent extends MediaActorFileForm.MediaActorFileFormEvent { + CloseEvent(MediaActorFileForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(MediaActorFileForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(MediaActorFileForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(MediaActorFileForm.CloseEvent.class, listener); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorFileView.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorFileView.java new file mode 100644 index 0000000..f1462b6 --- /dev/null +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorFileView.java @@ -0,0 +1,114 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.media.MediaConstants; +import de.thpeetz.kontor.media.data.MediaActorFile; +import de.thpeetz.kontor.media.services.MediaFileService; +import jakarta.annotation.security.PermitAll; +import lombok.Getter; +import org.springframework.context.annotation.Scope; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = MediaConstants.MEDIAACTORFILE_ROUTE, layout = MainLayout.class) +@PageTitle("MediaActorFile | Media | Kontor") +public class MediaActorFileView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(MediaActorFile.class); + @Getter + MediaActorFileForm form; + MediaFileService service; + + public MediaActorFileView(MediaFileService service) { + this.service = service; + addClassName("mediaactorfile-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("mediaactorfile-grid"); + grid.setSizeFull(); + grid.setColumns("id", "media_actor.name", "media_file.title"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editMediaActorFile(event.getValue())); + } + + private void configureForm() { + form = new MediaActorFileForm(service.findAllMediaFiles(null), service.findAllMediaActors(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveMediaActorFile); + form.addDeleteListener(this::deleteMediaActorFile); + form.addCloseListener(e -> closeEditor()); + } + + private void saveMediaActorFile(MediaActorFileForm.SaveEvent event) { + service.saveMediaActorFile(event.getMediaActorFile()); + updateList(); + closeEditor(); + } + + private void deleteMediaActorFile(MediaActorFileForm.DeleteEvent event) { + service.deleteMediaActorFile(event.getMediaActorFile()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + Button addMediaActorFileButton = new Button("Add MediaActorFile"); + addMediaActorFileButton.addClickListener(click -> addMediaActorFile()); + + HorizontalLayout toolbar = new HorizontalLayout(addMediaActorFileButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editMediaActorFile(MediaActorFile mediaActorFile) { + if (mediaActorFile == null) { + closeEditor(); + } else { + form.setMediaActorFile(mediaActorFile); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setMediaActorFile(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addMediaActorFile() { + grid.asSingleSelect().clear(); + editMediaActorFile(new MediaActorFile()); + } + + public void updateList() { + grid.setItems(service.findAllMediaActorFiles()); + } +} diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorForm.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorForm.java index e7cab60..e011fb7 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorForm.java +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorForm.java @@ -7,6 +7,7 @@ import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.listbox.MultiSelectListBox; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.data.binder.BeanValidationBinder; @@ -21,7 +22,7 @@ import java.util.List; public class MediaActorForm extends FormLayout { TextField name = new TextField("Name"); - // Grid mediaActorFiles = new Grid<>(MediaActorFile.class); + Grid mediaActorFiles = new Grid<>(MediaActorFile.class); Button save = new Button("Save"); Button delete = new Button("Delete"); @@ -33,11 +34,11 @@ public class MediaActorForm extends FormLayout { addClassName("media-actor-form"); binder.bindInstanceFields(this); - // mediaActorFiles.setColumns("media_file.title"); - // mediaActorFiles.getColumnByKey("mediaFile.title").setHeader("MediaFile"); - // mediaActorFiles.getColumns().forEach(col -> col.setAutoWidth(true)); - // add(name, mediaActorFiles, createButtonsLayout()); - add(name, createButtonsLayout()); + mediaActorFiles.setColumns("media_file.title"); + mediaActorFiles.getColumnByKey("media_file.title").setHeader("File Title"); + add(name, 2); + add(mediaActorFiles, 2); + add(createButtonsLayout()); } private HorizontalLayout createButtonsLayout() { @@ -67,8 +68,8 @@ public class MediaActorForm extends FormLayout { } public void setMediaActorFiles(List mediaActorFiles) { - log.info("Setting comic works: {}", mediaActorFiles); - // this.mediaActorFiles.setItems(mediaActorFiles); + log.info("Setting mediaActorFiles: {}", mediaActorFiles); + this.mediaActorFiles.setItems(mediaActorFiles); } public abstract static class MediaActorFormEvent extends ComponentEvent { diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorView.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorView.java index 40727cb..0f885b8 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorView.java +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorView.java @@ -10,8 +10,6 @@ import com.vaadin.flow.data.value.ValueChangeMode; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; import com.vaadin.flow.spring.annotation.SpringComponent; -import de.thpeetz.kontor.comics.data.Artist; -import de.thpeetz.kontor.comics.views.ArtistForm; import de.thpeetz.kontor.common.views.MainLayout; import de.thpeetz.kontor.media.MediaConstants; import de.thpeetz.kontor.media.data.MediaActor; @@ -55,7 +53,7 @@ public class MediaActorView extends VerticalLayout { private void configureForm() { form = new MediaActorForm(); - form.setWidth("25em"); + form.setWidth("75em"); form.setVisible(false); form.addSaveListener(this::saveMediaActor); form.addDeleteListener(this::deleteMediaActor); diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileForm.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileForm.java index b1042fa..4b4b469 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileForm.java +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileForm.java @@ -7,14 +7,18 @@ import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.checkbox.Checkbox; import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.data.binder.BeanValidationBinder; import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.media.data.MediaActorFile; import de.thpeetz.kontor.media.data.MediaFile; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import java.util.List; + @Slf4j public class MediaFileForm extends FormLayout { @@ -25,6 +29,7 @@ public class MediaFileForm extends FormLayout { TextField cloudLink = new TextField("Cloud Link"); Checkbox review = new Checkbox("Review"); Checkbox shouldDownload = new Checkbox("Download"); + Grid mediaActorFiles = new Grid<>(MediaActorFile.class); Button save = new Button("Save"); Button delete = new Button("Delete"); @@ -36,12 +41,18 @@ public class MediaFileForm extends FormLayout { addClassName("mediafile-form"); binder.bindInstanceFields(this); id.setReadOnly(true); + + mediaActorFiles.setColumns("media_actor.name"); + mediaActorFiles.getColumnByKey("media_actor.name").setHeader("Actor"); + add(id, 2); add(url, 2); add(title, 2); add(fileName, 2); add(cloudLink, 2); - add(review, shouldDownload, createButtonsLayout()); + add(review, shouldDownload); + add(mediaActorFiles, 2); + add(createButtonsLayout()); } private HorizontalLayout createButtonsLayout() { @@ -70,6 +81,10 @@ public class MediaFileForm extends FormLayout { binder.setBean(mediaFile); } + public void setMediaActorFiles(List actorFiles) { + mediaActorFiles.setItems(actorFiles); + } + @Getter public abstract static class MediaFileFormEvent extends ComponentEvent { private final MediaFile mediaFile; diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileView.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileView.java index 0784b8d..3793a38 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileView.java +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaFileView.java @@ -20,8 +20,10 @@ import de.thpeetz.kontor.media.data.MediaFile; import de.thpeetz.kontor.media.services.MediaFileService; import jakarta.annotation.security.RolesAllowed; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Scope; +@Slf4j @SpringComponent @Scope("prototype") @RolesAllowed("MEDIA") @@ -164,6 +166,12 @@ public class MediaFileView extends VerticalLayout { closeEditor(); } else { form.setMediaFile(mediaFile); + if (mediaFile.getMediaActorFiles() == null) { + log.info("no MediaActorFiles"); + } else { + log.info("MediaActorFiles size: {}", mediaFile.getMediaActorFiles().size()); + } + form.setMediaActorFiles(mediaFile.getMediaActorFiles()); form.setVisible(true); addClassName("editing"); } From 3bb3b22ceaf864806d22c56fe58b517100f7c6f1 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 10 Feb 2025 12:54:59 +0100 Subject: [PATCH 83/91] add MediaActor and MediaActorFile to kontor-schema --- python/kontor-schema/kontor_schema/media.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/python/kontor-schema/kontor_schema/media.py b/python/kontor-schema/kontor_schema/media.py index 5998c0a..d1e2158 100644 --- a/python/kontor-schema/kontor_schema/media.py +++ b/python/kontor-schema/kontor_schema/media.py @@ -5,14 +5,16 @@ from pathlib import Path import requests from bs4 import BeautifulSoup -from sqlalchemy import Column, DateTime, Integer, String +from sqlalchemy import Column, DateTime, Integer, String, ForeignKey from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship from .base import Base, BaseMixin, BaseVideoMixin class MediaFile(Base, BaseMixin, BaseVideoMixin): __tablename__ = 'media_file' + media_actor_files = relationship("MediaActorFile") def __repr__(self): return f'MediaFile({self.id} {self.title} {self.title})' @@ -66,6 +68,20 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin): return self.file_name +class MediaActor(Base, BaseMixin): + __tablename__ = 'media_actor' + name = Column(String(255)) + media_actor_files = relationship("MediaActorFile") + + +class MediaActorFile(Base, BaseMixin): + __tablename__ = 'media_actor_file' + media_actor_id = Column(String, ForeignKey("media_actor.id"), nullable=False) + media_actor = relationship("MediaActor", back_populates="media_actor_files") + media_file_id = Column(String, ForeignKey("media_file.id"), nullable=False) + media_file = relationship("MediaFile", back_populates="media_actor_files") + + class MediaArticle(Base, BaseMixin): __tablename__ = 'media_article' review = Column(BIT(1)) From d8eecb4dab0c76e5f29d32e1afb0c885db8d333c Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 10 Feb 2025 15:56:32 +0100 Subject: [PATCH 84/91] fix NPE in MediaActorView --- .../kontor-schema/kontor_schema/database.py | 6 ++++-- .../de/thpeetz/kontor/comics/data/Artist.java | 3 ++- .../kontor/comics/views/ArtistView.java | 19 +++++++------------ .../thpeetz/kontor/media/data/MediaActor.java | 3 ++- .../thpeetz/kontor/media/data/MediaFile.java | 3 ++- .../kontor/media/views/MediaActorView.java | 2 +- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/python/kontor-schema/kontor_schema/database.py b/python/kontor-schema/kontor_schema/database.py index c4607f6..4627d87 100644 --- a/python/kontor-schema/kontor_schema/database.py +++ b/python/kontor-schema/kontor_schema/database.py @@ -14,7 +14,7 @@ from .comic import Issue, TradePaperback, StoryArc, Volume, ComicWork, Artist, C from .bookshelf import ArticleAuthor, BookAuthor, BookshelfPublisher, Article, Book, Author from .admin import Mail, MailAccount, ModuleData, Role, User, Token, AuthorizationMatrix from .metadata import MetaDataTable, MetaDataColumn -from .media import MediaVideo, MediaArticle, MediaFile +from .media import MediaVideo, MediaArticle, MediaFile, MediaActor, MediaActorFile class ColumnEntry(Enum): @@ -68,6 +68,8 @@ class KontorDB: self.registry[Book.__tablename__] = Book self.registry[Author.__tablename__] = Author self.registry[MediaFile.__tablename__] = MediaFile + self.registry[MediaActor.__tablename__] = MediaActor + self.registry[MediaActorFile.__tablename__] = MediaActorFile self.registry[MediaArticle.__tablename__] = MediaArticle self.registry[MediaVideo.__tablename__] = MediaVideo self.registry[MetaDataColumn.__tablename__] = MetaDataColumn @@ -217,7 +219,7 @@ class KontorDB: export_file = Path(export_file_name) case "SQLite": export_file = Path(export_file_name) - self.log.info("%d tables exported", len(results)) + self.log.info(f"{len(results)} tables exported") return results def import_db(self, import_file_name: str) -> dict: diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/data/Artist.java b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Artist.java index 6a3159d..3c2286f 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/comics/data/Artist.java +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/data/Artist.java @@ -1,5 +1,6 @@ package de.thpeetz.kontor.comics.data; +import java.util.LinkedList; import java.util.List; import de.thpeetz.kontor.common.data.AbstractEntity; @@ -32,7 +33,7 @@ public class Artist extends AbstractEntity { @OneToMany(fetch = FetchType.EAGER, mappedBy = "artist", cascade = CascadeType.REFRESH, orphanRemoval = true) @Nullable - List comicWorks; + List comicWorks = new LinkedList<>(); @Override public String toString() { diff --git a/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java index f264aa9..1a6b2a5 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java +++ b/springboot/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java @@ -1,5 +1,6 @@ package de.thpeetz.kontor.comics.views; +import lombok.Getter; import org.springframework.context.annotation.Scope; import com.vaadin.flow.component.Component; @@ -25,10 +26,12 @@ import jakarta.annotation.security.PermitAll; @PageTitle("Artist | Comics | Kontor") public class ArtistView extends VerticalLayout { - Grid grid = new Grid<>(Artist.class); - TextField filterText = new TextField(); - ArtistForm form; - ComicService service; + @Getter + Grid grid = new Grid<>(Artist.class); + TextField filterText = new TextField(); + @Getter + ArtistForm form; + ComicService service; public ArtistView(ComicService service) { this.service = service; @@ -41,14 +44,6 @@ public class ArtistView extends VerticalLayout { updateList(); } - public Grid getGrid() { - return grid; - } - - public ArtistForm getForm() { - return form; - } - private void configureGrid() { grid.addClassName("artist-grid"); grid.setSizeFull(); diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActor.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActor.java index 25cc196..cfc6270 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActor.java +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaActor.java @@ -9,6 +9,7 @@ import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import java.util.LinkedList; import java.util.List; @Getter @@ -24,5 +25,5 @@ public class MediaActor extends AbstractEntity { @OneToMany(fetch = FetchType.EAGER, mappedBy = "media_actor", cascade = CascadeType.REFRESH, orphanRemoval = true) @Nullable - List mediaActorFiles; + List mediaActorFiles = new LinkedList<>(); } diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java index 39be1c2..db1b826 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java +++ b/springboot/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java @@ -8,6 +8,7 @@ import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import java.util.LinkedList; import java.util.List; @Slf4j @@ -39,5 +40,5 @@ public class MediaFile extends AbstractEntity { @OneToMany(fetch = FetchType.EAGER, mappedBy = "media_file", cascade = CascadeType.REFRESH, orphanRemoval = true) @Nullable - List mediaActorFiles; + List mediaActorFiles = new LinkedList<>(); } diff --git a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorView.java b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorView.java index 0f885b8..23ca35e 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorView.java +++ b/springboot/src/main/java/de/thpeetz/kontor/media/views/MediaActorView.java @@ -44,7 +44,7 @@ public class MediaActorView extends VerticalLayout { } private void configureGrid() { - grid.addClassName("artist-grid"); + grid.addClassName("media-actor-grid"); grid.setSizeFull(); grid.setColumns("name"); grid.getColumns().forEach(col -> col.setAutoWidth(true)); From 0d1b2e416e80d96b23579d01d87e999f3e4f8062 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Tue, 11 Feb 2025 10:39:07 +0100 Subject: [PATCH 85/91] add tab witg table and details --- python/kontor-gui/gui/media_window.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/python/kontor-gui/gui/media_window.py b/python/kontor-gui/gui/media_window.py index accbf43..49f4d6c 100644 --- a/python/kontor-gui/gui/media_window.py +++ b/python/kontor-gui/gui/media_window.py @@ -1,5 +1,5 @@ from PySide6.QtCore import Signal, QSortFilterProxyModel -from PySide6.QtWidgets import QMdiSubWindow, QWidget, QVBoxLayout, QTabWidget, QTableView, QHeaderView +from PySide6.QtWidgets import QMdiSubWindow, QWidget, QVBoxLayout, QTabWidget, QTableView, QHeaderView, QLabel from gui.model_config import KontorModelConfig from gui.table_model import KontorTableModel @@ -25,6 +25,7 @@ class MediaWindow(QMdiSubWindow): self.tabs.addTab(self.generate_data_tab("media_file"), "Media File") self.tabs.addTab(self.generate_data_tab("media_video"), "Media Video") self.tabs.addTab(self.generate_data_tab("media_article"), "Media Article") + self.tabs.addTab(self.generate_data_tab_with_details("media_actor"), "Media Actor") self.tabs.currentChanged.connect(self._tab_changed) layout.addWidget(self.tabs) self.setLayout(layout) @@ -63,3 +64,24 @@ class MediaWindow(QMdiSubWindow): model.refresh() table_view.resizeColumnToContents(0) return data_tab + + def generate_data_tab_with_details(self, table_name): + data_tab = QWidget() + + table_config = KontorModelConfig(self._main_window.kontor_db, self, table_name) + model = KontorTableModel(table_config) + layout = QVBoxLayout() + self.data_views.append(model) + data_tab.setLayout(layout) + table_view = QTableView() + proxy_model = QSortFilterProxyModel() + proxy_model.setSourceModel(model) + table_view.setSortingEnabled(True) + table_view.setModel(proxy_model) + layout.addLayout(table_config.get_filter_layout()) + layout.addWidget(table_view) + layout.addWidget(QLabel("test test")) + model.refresh() + table_view.resizeColumnToContents(0) + return data_tab + From 41e2b11da29c069dba8c04dccaa759ba20c08cf9 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Tue, 11 Feb 2025 16:16:38 +0100 Subject: [PATCH 86/91] add details view --- python/kontor-gui/gui/media_window.py | 27 +++++-------- python/kontor-gui/gui/table_details.py | 52 ++++++++++++++++++++++++++ python/kontor-gui/gui/table_model.py | 8 ++++ 3 files changed, 69 insertions(+), 18 deletions(-) create mode 100644 python/kontor-gui/gui/table_details.py diff --git a/python/kontor-gui/gui/media_window.py b/python/kontor-gui/gui/media_window.py index 49f4d6c..080d5cd 100644 --- a/python/kontor-gui/gui/media_window.py +++ b/python/kontor-gui/gui/media_window.py @@ -1,8 +1,10 @@ from PySide6.QtCore import Signal, QSortFilterProxyModel -from PySide6.QtWidgets import QMdiSubWindow, QWidget, QVBoxLayout, QTabWidget, QTableView, QHeaderView, QLabel +from PySide6.QtWidgets import QMdiSubWindow, QWidget, QVBoxLayout, QTabWidget, QTableView, QHeaderView, QLabel, \ + QHBoxLayout, QFormLayout, QLineEdit -from gui.model_config import KontorModelConfig -from gui.table_model import KontorTableModel +from .model_config import KontorModelConfig +from .table_details import KontorTableDetailsView +from .table_model import KontorTableModel class MediaWindow(QMdiSubWindow): @@ -66,22 +68,11 @@ class MediaWindow(QMdiSubWindow): return data_tab def generate_data_tab_with_details(self, table_name): - data_tab = QWidget() - table_config = KontorModelConfig(self._main_window.kontor_db, self, table_name) model = KontorTableModel(table_config) - layout = QVBoxLayout() self.data_views.append(model) - data_tab.setLayout(layout) - table_view = QTableView() - proxy_model = QSortFilterProxyModel() - proxy_model.setSourceModel(model) - table_view.setSortingEnabled(True) - table_view.setModel(proxy_model) - layout.addLayout(table_config.get_filter_layout()) - layout.addWidget(table_view) - layout.addWidget(QLabel("test test")) - model.refresh() - table_view.resizeColumnToContents(0) - return data_tab + details_view = KontorTableDetailsView(model) + return details_view.data_view + def cell_selected(self, item): + self.log.info(f"Cell {item.row()}:{item.column()} clicked") diff --git a/python/kontor-gui/gui/table_details.py b/python/kontor-gui/gui/table_details.py new file mode 100644 index 0000000..a0e7cef --- /dev/null +++ b/python/kontor-gui/gui/table_details.py @@ -0,0 +1,52 @@ +from PySide6.QtCore import QSortFilterProxyModel +from PySide6.QtWidgets import QHBoxLayout, QWidget, QTableView, QVBoxLayout, QFormLayout, QLineEdit, QLabel + +from .table_model import KontorTableModel + + +class KontorTableDetailsView: + def __init__(self, table_model: KontorTableModel): + self._data_view: QWidget = QWidget() + self._model = table_model + self._label = QLabel() + self.init_gui() + + def init_gui(self): + layout = QVBoxLayout() + self._data_view.setLayout(layout) + details_layout = QHBoxLayout() + table_with_details = QWidget() + table_with_details.setLayout(details_layout) + + table_view = QTableView() + table_view.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows) + proxy_model = QSortFilterProxyModel() + proxy_model.setSourceModel(self._model) + table_view.setSortingEnabled(True) + table_view.setModel(proxy_model) + table_view.clicked.connect(self.update_details) + + layout.addLayout(self._model.config.get_filter_layout()) + details_layout.addWidget(table_view) + + form = QWidget() + form_layout = QFormLayout(form) + form.setLayout(form_layout) + + title = QLineEdit(form) + form_layout.addRow("ID", self._label) + form_layout.addRow("Title", title) + # layout.addWidget(table_view) + details_layout.addWidget(form) + layout.addWidget(table_with_details) + self._model.refresh() + table_view.resizeColumnToContents(0) + + @property + def data_view(self): + return self._data_view + + def update_details(self, item): + print(f"Cell {item.row()}-{item.column()} selected") + self._model.log.info(f"Cell {item.row()}-{item.column()} selected") + self._label.setText(self._model.raw_data()[item.row()][0]) diff --git a/python/kontor-gui/gui/table_model.py b/python/kontor-gui/gui/table_model.py index 24290c1..fb32802 100644 --- a/python/kontor-gui/gui/table_model.py +++ b/python/kontor-gui/gui/table_model.py @@ -52,6 +52,14 @@ class KontorTableModel(QAbstractTableModel): def __str__(self): return f"KontorTableModel({self._config})" + @property + def config(self): + return self._config + + @property + def raw_data(self): + return self._data + def refresh(self): # self.log.info("refresh") data = self._config.get_data() From 7db244f59986e79810ddeb6449fc9e9bf2faeb74 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Wed, 12 Feb 2025 18:07:48 +0100 Subject: [PATCH 87/91] add length vor varchar for _id fields --- python/kontor-gui/gui/table_details.py | 24 ++++++++++++++------- python/kontor-schema/kontor_schema/media.py | 4 ++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/python/kontor-gui/gui/table_details.py b/python/kontor-gui/gui/table_details.py index a0e7cef..07760f0 100644 --- a/python/kontor-gui/gui/table_details.py +++ b/python/kontor-gui/gui/table_details.py @@ -8,26 +8,29 @@ class KontorTableDetailsView: def __init__(self, table_model: KontorTableModel): self._data_view: QWidget = QWidget() self._model = table_model + self.log = table_model.log + self._table_view = QTableView() self._label = QLabel() self.init_gui() def init_gui(self): + self.log.info("KontorTableDetailsView.init_gui()") layout = QVBoxLayout() self._data_view.setLayout(layout) details_layout = QHBoxLayout() table_with_details = QWidget() table_with_details.setLayout(details_layout) - table_view = QTableView() - table_view.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows) + self._table_view.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows) proxy_model = QSortFilterProxyModel() proxy_model.setSourceModel(self._model) - table_view.setSortingEnabled(True) - table_view.setModel(proxy_model) - table_view.clicked.connect(self.update_details) + self._table_view.setSortingEnabled(True) + self._table_view.setModel(proxy_model) + self._table_view.clicked.connect(self.update_details) + self._table_view.activated.connect(self.refresh_details) layout.addLayout(self._model.config.get_filter_layout()) - details_layout.addWidget(table_view) + details_layout.addWidget(self._table_view) form = QWidget() form_layout = QFormLayout(form) @@ -40,7 +43,7 @@ class KontorTableDetailsView: details_layout.addWidget(form) layout.addWidget(table_with_details) self._model.refresh() - table_view.resizeColumnToContents(0) + self._table_view.resizeColumnToContents(0) @property def data_view(self): @@ -48,5 +51,10 @@ class KontorTableDetailsView: def update_details(self, item): print(f"Cell {item.row()}-{item.column()} selected") - self._model.log.info(f"Cell {item.row()}-{item.column()} selected") + self.log.info(f"Cell {item.row()}-{item.column()} selected") self._label.setText(self._model.raw_data()[item.row()][0]) + + def refresh_details(self): + indexes = self._table_view.selectedIndexes() + for index in indexes: + self.log.info(f"refresh_details: Cell {index.row()}-{index.column()} selected") diff --git a/python/kontor-schema/kontor_schema/media.py b/python/kontor-schema/kontor_schema/media.py index d1e2158..bb2661e 100644 --- a/python/kontor-schema/kontor_schema/media.py +++ b/python/kontor-schema/kontor_schema/media.py @@ -76,9 +76,9 @@ class MediaActor(Base, BaseMixin): class MediaActorFile(Base, BaseMixin): __tablename__ = 'media_actor_file' - media_actor_id = Column(String, ForeignKey("media_actor.id"), nullable=False) + media_actor_id = Column(String(255), ForeignKey("media_actor.id"), nullable=False) media_actor = relationship("MediaActor", back_populates="media_actor_files") - media_file_id = Column(String, ForeignKey("media_file.id"), nullable=False) + media_file_id = Column(String(255), ForeignKey("media_file.id"), nullable=False) media_file = relationship("MediaFile", back_populates="media_actor_files") From 45971518eef3507f16c9aa354a646d6b816f77dc Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 17 Mar 2025 17:44:49 +0100 Subject: [PATCH 88/91] change ModuleData view --- .../kontor/admin/views/ModuleDataView.java | 58 +++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/springboot/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataView.java b/springboot/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataView.java index bc335f3..caf3c2f 100644 --- a/springboot/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataView.java +++ b/springboot/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataView.java @@ -2,7 +2,12 @@ package de.thpeetz.kontor.admin.views; import com.vaadin.flow.component.Component; import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.contextmenu.ContextMenu; +import com.vaadin.flow.component.contextmenu.MenuItem; import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.TextField; @@ -14,6 +19,7 @@ import de.thpeetz.kontor.admin.data.ModuleData; import de.thpeetz.kontor.admin.services.ModuleService; import de.thpeetz.kontor.common.views.MainLayout; import jakarta.annotation.security.RolesAllowed; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Scope; @@ -25,11 +31,35 @@ import org.springframework.context.annotation.Scope; @PageTitle("Module Data | Admin | Kontor") public class ModuleDataView extends VerticalLayout { - Grid grid = new Grid<>(ModuleData.class); + Grid grid = new Grid<>(ModuleData.class, false); + Grid.Column idColumn = grid.addColumn(ModuleData::getId) + .setHeader("ID").setResizable(true).setSortable(true); + Grid.Column nameColumn = grid.addColumn(ModuleData::getModuleName) + .setHeader("Name").setResizable(true).setSortable(true); + Grid.Column importColumn = grid.addComponentColumn(moduleData -> createStatusIcon(moduleData.getImportData())) + .setHeader("Import Data").setWidth("6rem").setSortable(true); + TextField filterText = new TextField(); + @Getter ModuleDataForm form; ModuleService service; + private static class ColumnToggleContextMenu extends ContextMenu { + public ColumnToggleContextMenu(Component target) { + super(target); + setOpenOnClick(true); + } + + void addColumnToggleItem(String label, Grid.Column column) { + MenuItem menuItem = this.addItem(label, e -> { + column.setVisible(e.getSource().isChecked()); + }); + menuItem.setCheckable(true); + menuItem.setChecked(column.isVisible()); + menuItem.setKeepOpen(true); + } + } + public ModuleDataView(ModuleService service) { this.service = service; addClassName("moduleData-view"); @@ -44,7 +74,7 @@ public class ModuleDataView extends VerticalLayout { private void configureGrid() { grid.addClassName("moduleData-grid"); grid.setSizeFull(); - grid.setColumns("moduleName", "importData"); + //grid.setColumns("moduleName", "importData"); grid.getColumns().forEach(col -> col.setAutoWidth(true)); grid.asSingleSelect().addValueChangeListener(event -> editModuleData(event.getValue())); } @@ -58,7 +88,20 @@ public class ModuleDataView extends VerticalLayout { form.addCloseListener(e -> closeEditor()); } - private void saveModuleData(ModuleDataForm.SaveEvent event) { + private Icon createStatusIcon(boolean status) { + Icon icon; + if (status) { + icon = VaadinIcon.CHECK.create(); + icon.getElement().getThemeList().add("badge success"); + } else { + icon = VaadinIcon.CLOSE_SMALL.create(); + icon.getElement().getThemeList().add("badge error"); + } + icon.getStyle().set("padding", "var(--lumo-space-xs"); + return icon; + } + + private void saveModuleData(ModuleDataForm.SaveEvent event) { ModuleData moduleData = event.getModuleData(); service.saveModuleData(moduleData); updateList(); @@ -86,7 +129,14 @@ public class ModuleDataView extends VerticalLayout { filterText.setValueChangeMode(ValueChangeMode.LAZY); filterText.addValueChangeListener(e -> updateList()); Button addModuleDataButton = new Button("Add module", click -> addModuleData()); - HorizontalLayout toolbar = new HorizontalLayout(filterText, addModuleDataButton); + + Button menuButton = new Button("Show/Hide Columns"); + menuButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + ColumnToggleContextMenu columnToggleContextMenu = new ColumnToggleContextMenu(menuButton); + columnToggleContextMenu.addColumnToggleItem("ID", idColumn); + columnToggleContextMenu.addColumnToggleItem("Module Name", nameColumn); + columnToggleContextMenu.addColumnToggleItem("Import Data", importColumn); + HorizontalLayout toolbar = new HorizontalLayout(filterText, addModuleDataButton, menuButton); toolbar.addClassName("toolbar"); return toolbar; } From a5cdf8867ae5837155f3a80fcd0e98549a254343 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sat, 29 Mar 2025 19:05:31 +0100 Subject: [PATCH 89/91] move Git repository kontor-docker to directory bottle-docker --- bottle-docker/.gitignore | 129 ++++++++++++ bottle-docker/README.md | 2 + bottle-docker/app/Dockerfile | 19 ++ bottle-docker/app/requirements.txt | 2 + bottle-docker/app/src/comics/__init__.py | 193 ++++++++++++++++++ bottle-docker/app/src/comics/artistDAO.py | 49 +++++ bottle-docker/app/src/comics/comicDAO.py | 52 +++++ bottle-docker/app/src/comics/models.py | 121 +++++++++++ bottle-docker/app/src/comics/publisherDAO.py | 49 +++++ bottle-docker/app/src/comics/storyArcDAO.py | 49 +++++ bottle-docker/app/src/kontor.css | 89 ++++++++ bottle-docker/app/src/kontor.py | 148 ++++++++++++++ bottle-docker/app/src/sessionDAO.py | 64 ++++++ bottle-docker/app/src/userDAO.py | 75 +++++++ bottle-docker/app/src/views/artist_list.tpl | 39 ++++ .../app/src/views/artist_template.tpl | 43 ++++ bottle-docker/app/src/views/comic_index.tpl | 34 +++ bottle-docker/app/src/views/comic_list.tpl | 44 ++++ .../app/src/views/comic_template.tpl | 51 +++++ bottle-docker/app/src/views/kontor.tpl | 27 +++ bottle-docker/app/src/views/login.tpl | 52 +++++ .../app/src/views/publisher_list.tpl | 39 ++++ .../app/src/views/publisher_template.tpl | 42 ++++ bottle-docker/app/src/views/signup.tpl | 59 ++++++ bottle-docker/app/src/views/storyarc_list.tpl | 40 ++++ .../app/src/views/storyarc_template.tpl | 43 ++++ bottle-docker/docker-compose.yaml | 31 +++ bottle-docker/front-end/index.html | 82 ++++++++ bottle-docker/nginx/nginx.conf | 14 ++ 29 files changed, 1681 insertions(+) create mode 100644 bottle-docker/.gitignore create mode 100644 bottle-docker/README.md create mode 100644 bottle-docker/app/Dockerfile create mode 100644 bottle-docker/app/requirements.txt create mode 100644 bottle-docker/app/src/comics/__init__.py create mode 100644 bottle-docker/app/src/comics/artistDAO.py create mode 100644 bottle-docker/app/src/comics/comicDAO.py create mode 100644 bottle-docker/app/src/comics/models.py create mode 100644 bottle-docker/app/src/comics/publisherDAO.py create mode 100644 bottle-docker/app/src/comics/storyArcDAO.py create mode 100644 bottle-docker/app/src/kontor.css create mode 100644 bottle-docker/app/src/kontor.py create mode 100644 bottle-docker/app/src/sessionDAO.py create mode 100644 bottle-docker/app/src/userDAO.py create mode 100644 bottle-docker/app/src/views/artist_list.tpl create mode 100644 bottle-docker/app/src/views/artist_template.tpl create mode 100644 bottle-docker/app/src/views/comic_index.tpl create mode 100644 bottle-docker/app/src/views/comic_list.tpl create mode 100644 bottle-docker/app/src/views/comic_template.tpl create mode 100644 bottle-docker/app/src/views/kontor.tpl create mode 100644 bottle-docker/app/src/views/login.tpl create mode 100644 bottle-docker/app/src/views/publisher_list.tpl create mode 100644 bottle-docker/app/src/views/publisher_template.tpl create mode 100644 bottle-docker/app/src/views/signup.tpl create mode 100644 bottle-docker/app/src/views/storyarc_list.tpl create mode 100644 bottle-docker/app/src/views/storyarc_template.tpl create mode 100644 bottle-docker/docker-compose.yaml create mode 100644 bottle-docker/front-end/index.html create mode 100644 bottle-docker/nginx/nginx.conf diff --git a/bottle-docker/.gitignore b/bottle-docker/.gitignore new file mode 100644 index 0000000..b6e4761 --- /dev/null +++ b/bottle-docker/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/bottle-docker/README.md b/bottle-docker/README.md new file mode 100644 index 0000000..2c49794 --- /dev/null +++ b/bottle-docker/README.md @@ -0,0 +1,2 @@ +# kontor-bottle +Kontor with Python Bottle Framework diff --git a/bottle-docker/app/Dockerfile b/bottle-docker/app/Dockerfile new file mode 100644 index 0000000..2974a07 --- /dev/null +++ b/bottle-docker/app/Dockerfile @@ -0,0 +1,19 @@ +# set base image (host OS) +FROM python:3.8 + +# set the working directory in the container +WORKDIR /code + +# copy the dependencies file to the working directory +COPY requirements.txt /code/ + +# copy the content of the local src directory to the working directory +COPY src/ /code/ + +# install dependencies +RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt + +EXPOSE 9000 + +# command to run on container start +CMD [ "python", "./kontor.py" ] diff --git a/bottle-docker/app/requirements.txt b/bottle-docker/app/requirements.txt new file mode 100644 index 0000000..8e5db27 --- /dev/null +++ b/bottle-docker/app/requirements.txt @@ -0,0 +1,2 @@ +pymongo +bottle diff --git a/bottle-docker/app/src/comics/__init__.py b/bottle-docker/app/src/comics/__init__.py new file mode 100644 index 0000000..6afff02 --- /dev/null +++ b/bottle-docker/app/src/comics/__init__.py @@ -0,0 +1,193 @@ +# -*- coding:utf-8 -*- +import pymongo +import comics.publisherDAO +import comics.artistDAO +import comics.comicDAO +import bottle +import cgi + + +__author__ = 'tpeetz' + + +class Plugin: + + def __init__(self, app, database, sessions): + self.app = app + self.db = database + self.sessions = sessions + self.publishers = publisherDAO.PublisherDAO(database) + self.artists = artistDAO.ArtistDAO(database) + self.comics = comicDAO.ComicDAO(database) + self.routing() + + + def routing(self): + self.app.route('/comics/', 'GET', self.comic_index) + self.app.route('/comics/comic', 'GET', self.comic_list) + self.app.route('/comics/comic/', 'GET', self.comic_details) + self.app.route('/comics/comic/create', 'GET', self.get_comic_create) + self.app.route('/comics/comic/create', 'POST', self.post_create_comic) + self.app.route('/comics/publisher', 'GET', self.publisher_list) + self.app.route('/comics/publisher/', 'GET', self.publisher_details) + self.app.route('/comics/publisher/create', 'GET', self.get_publisher_create) + self.app.route('/comics/publisher/create', 'POST', self.post_create_publisher) + self.app.route('/comics/artist', 'GET', self.artist_list) + self.app.route('/comics/artist/', 'GET', self.artist_details) + self.app.route('/comics/artist/create', 'GET', self.get_artist_create) + self.app.route('/comics/artist/create', 'POST', self.post_create_artist) + self.app.route('/comics/storyarc', 'GET', self.storyarc_list) + self.app.route('/comics/storyarc/', 'GET', self.storyarc_details) + self.app.route('/comics/storyarc/create', 'GET', self.get_storyarc_create) + self.app.route('/comics/storyarc/create', 'POST', self.post_create_storyarc) + + def comic_index(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + return bottle.template('comic_index', dict(username=username)) + + + def comic_list(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + l = self.comics.get_comics() + return bottle.template('comic_list', dict(comics=l, username=username)) + + + def comic_details(self, id): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + comic = self.comics.get_comic(id) + errors = "" + if comic == None: + errors = "Entry not found" + return bottle.template('comic_template', dict(title=comic['title'], + id=comic['_id'], + current_order=comic['current_order'], + completed=comic['completed'], + errors="", + username=username)) + + + def get_comic_create(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + return bottle.template("comic_template", dict(title="", + id='newentry', + current_order=False, + completed=False, + errors="", + username=username)) + + + def post_create_comic(self): + comic_id = bottle.request.forms.get("id") + comic_title = bottle.request.forms.get("title") + comic_order = bottle.request.forms.get("current_order") + comic_completed = bottle.request.forms.get("completed") + if comic_id == "newentry": + self.comics.insert_entry(comic_title, None, comic_order, comic_completed) + else: + self.comics.update_entry(comic_id, comic_title, None, comic_order, comic_completed) + bottle.redirect("/comics/comic") + + + def publisher_list(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + l = self.publishers.get_publishers() + return bottle.template('publisher_list', dict(publishers=l, username=username)) + + + def publisher_details(self, id): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + publisher = self.publishers.get_publisher(id) + errors = "" + if publisher == None: + errors= "Entry not found" + return bottle.template('publisher_template', dict(name=publisher['name'], id=publisher['_id'], errors="", username=username)) + + + def get_publisher_create(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + return bottle.template("publisher_template", dict(name="", id='newentry', errors="", username=username)) + + + def post_create_publisher(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + publisher_id = bottle.request.forms.get("id") + publisher_name = bottle.request.forms.get("name") + if publisher_id == "newentry": + self.publishers.insert_entry(publisher_name) + else: + self.publishers.update_entry(publisher_id, publisher_name) + bottle.redirect("/comics/publisher") + + + def artist_list(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + l = self.artists.get_artists() + return bottle.template('artist_list', dict(artists=l, username=username)) + + + def artist_details(self, id): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + artist = self.artists.get_artist(id) + errors = "" + if artist == None: + errors= "Entry not found" + return bottle.template('artist_template', dict(name=artist['name'], id=artist['_id'], errors="", username=username)) + + + def get_artist_create(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + return bottle.template("artist_template", dict(name="", id='newentry', errors="", username=username)) + + + def post_create_artist(self): + artist_id = bottle.request.forms.get("id") + artist_name = bottle.request.forms.get("name") + if artist_id == "newentry": + self.artists.insert_entry(artist_name) + else: + self.artists.update_entry(artist_id, artist_name) + bottle.redirect("/comics/artist") + + + def storyarc_list(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + l = self.storyarcs.get_storyarcs() + return bottle.template('storyarc_list', dict(storyarcs=l, username=username)) + + + def storyarc_details(self, id): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + storyarc = self.storyarcs.get_storyarc(id) + errors = "" + if storyarc == None: + errors = "Entry not found" + return bottle.template('storyarc_template', dict(title=storyarc['title'], id=storyarc['_id'], errors="", username=username)) + + + def get_storyarc_create(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + return bottle.template("storyarc_template", dict(title="", id='newentry', errors="", username=username)) + + + def post_create_storyarc(self): + storyarc_id = bottle.request.forms.get("id") + storyarc_title = bottle.request.forms.get("title") + if storyarc_id == "newentry": + self.storyarcs.insert_entry(storyarc_title) + else: + self.storyarcs.update_entry(storyarc_id, storyarc_title) + bottle.redirect("/comics/storyarc") diff --git a/bottle-docker/app/src/comics/artistDAO.py b/bottle-docker/app/src/comics/artistDAO.py new file mode 100644 index 0000000..3ef93b9 --- /dev/null +++ b/bottle-docker/app/src/comics/artistDAO.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +from bson.objectid import ObjectId +import sys + +class ArtistDAO: + + # constructor for the class + def __init__(self, database): + self.db = database + self.artists = database.artists + + def get_artists(self): + cursor = self.artists.find() + l = [] + for artist in cursor: + l.append({'name':artist['name'], '_id':artist['_id']}) + return l + + def get_artist(self, artist_id): + artist = self.artists.find_one({"_id": ObjectId(artist_id)}) + return artist + + def insert_entry(self, artist_name): + print("inserting artist entry", artist_name) + + artist = {"name": artist_name} + + # now insert the post + try: + result = self.artists.insert_one(artist) + print("Matching artist: ", result.matched_count) + print("Modified artist: ", result.modified_count) + except: + print("Error inserting artist") + print("Unexpected error:", sys.exc_info()) + + def update_entry(self, artist_id, artist_name): + print("upserting artist entry", artist_name) + + filter_doc = {"_id": ObjectId(artist_id)} + artist = { "$set": {"name": artist_name}} + + # now insert the post + try: + result = self.artists.update_one(filter_doc, artist) + print("Modified artist: ", result.modified_count) + except: + print("Error inserting artist") + print("Unexpected error:", sys.exc_info()) diff --git a/bottle-docker/app/src/comics/comicDAO.py b/bottle-docker/app/src/comics/comicDAO.py new file mode 100644 index 0000000..82c122b --- /dev/null +++ b/bottle-docker/app/src/comics/comicDAO.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +from bson.objectid import ObjectId +import sys + +class ComicDAO: + + # constructor for the class + def __init__(self, database): + self.db = database + self.comics = database.comics + + def get_comics(self): + cursor = self.comics.find() + l = [] + for comic in cursor: + l.append({'title':comic['title'], + '_id':comic['_id'], + 'current_order':comic['current_order'], + 'completed':comic['completed']}) + return l + + def get_comic(self, comic_id): + comic = self.comics.find_one({"_id": ObjectId(comic_id)}) + return comic + + def insert_entry(self, comic_title, comic_publisher=None, comic_order=False, comic_completed=False): + print("inserting comic entry", comic_title) + + comic = {"title": comic_title, "current_order": comic_order, "completed": comic_completed} + + # now insert the comic + try: + result = self.comics.insert_one(comic) + print("Modified comic: ", result.modified_count) + except: + print("Error inserting comic") + print("Unexpected error:", sys.exc_info()) + + def update_entry(self, comic_id, comic_title, comic_publisher=None, comic_order=False, comic_completed=False): + print("upserting comic entry", comic_title) + + filter_doc = {"_id": ObjectId(comic_id)} + comic = { "$set": {"title": comic_title, 'current_order': comic_order, 'completed': comic_completed}} + + # now insert the post + try: + result = self.comics.update_one(filter_doc, comic) + print("Matching comic: ", result.matched_count) + print("Modified comic: ", result.modified_count) + except: + print("Error inserting comic") + print("Unexpected error:", sys.exc_info()) \ No newline at end of file diff --git a/bottle-docker/app/src/comics/models.py b/bottle-docker/app/src/comics/models.py new file mode 100644 index 0000000..1cca3b1 --- /dev/null +++ b/bottle-docker/app/src/comics/models.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +from mongoengine import connect, Document +from mongoengine import StringField, BooleanField +from mongoengine import ListField, ReferenceField, GenericReferenceField +import pprint + + +class Publisher(Document): + name = StringField(required=True) + + def __str__(self): + s = "Publisher(%s)" % self.name + return s + + +class Artist(Document): + name = StringField(required=True) + className = StringField() + + def __str__(self): + s = "Artist(%s)" % self.name + return s + + +class Issue(Document): + number = StringField() + comic = GenericReferenceField() + is_read = BooleanField(default=False) + is_stock = BooleanField(default=False) + + def __str__(self): + s = "Issue(%s # %s, %s)" % (self.comic.title, self.number, self.is_read) + return s + + +class StoryArc(Document): + name = StringField(required=True) + comic = GenericReferenceField() + issues = ListField(ReferenceField(Issue)) + + def __str__(self): + s = "StoryArc(%s, %s)" % (self.name, self.comic.title) + return s + + +class Volume(Document): + name = StringField(required=True) + comic = GenericReferenceField() + issues = ListField(ReferenceField(Issue)) + + def __str__(self): + s = "Volume(%s, %s, %s)" % (self.id, self.name, self.comic.title) + return s + + +class Comic(Document): + title = StringField(required=True) + publisher = ReferenceField(Publisher) + current_order = BooleanField() + completed = BooleanField() + issues = ListField(ReferenceField(Issue)) + stories = ListField(ReferenceField(StoryArc)) + + def __str__(self): + if self.publisher is None: + s = "Comic(%s, %s, %s, %s)" % (self.title, self.publisher, self.current_order, self.completed) + return s + else: + s = "Comic(%s, %s, %s, %s)" % ( + self.title, self.publisher.name, self.current_order, self.completed) + return s + + +class TradePaperback(Document): + comic = ReferenceField(Comic) + issue_start = StringField() + issue_end = StringField() + + def __str__(self): + s = "TPB(%s)" % self.comic.title + return s + + +def get_publisher(name): + publisher = Publisher.objects(name=name) + if publisher.count() > 0: + return publisher[0] + else: + return None + + +def get_comic(title): + comic = Comic.objects(title=title) + if comic.count() > 0: + return comic[0] + else: + return None + + +def get_issue(title, number): + comic = get_comic(title) + issues = Issue.objects(number=number, comic=comic) + if issues.count() > 0: + return issues[0] + else: + return None + +if __name__ == '__main__': + connect('comics') + for publisher in Publisher.objects: + pprint.pprint(publisher) + for artist in Artist.objects: + pprint.pprint(artist) + for comic in Comic.objects: + pprint.pprint(comic) + for issue in Issue.objects: + pprint.pprint(issue) + for story in StoryArc.objects: + pprint.pprint(story) + for tpb in TradePaperback.objects: + pprint.pprint(tpb) diff --git a/bottle-docker/app/src/comics/publisherDAO.py b/bottle-docker/app/src/comics/publisherDAO.py new file mode 100644 index 0000000..f6a3cfe --- /dev/null +++ b/bottle-docker/app/src/comics/publisherDAO.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +from bson.objectid import ObjectId +import sys + +class PublisherDAO: + + # constructor for the class + def __init__(self, database): + self.db = database + self.publishers = database.publishers + + def get_publishers(self): + cursor = self.publishers.find() + l = [] + for publisher in cursor: + l.append({'name':publisher['name'], '_id':publisher['_id']}) + return l + + def get_publisher(self, publisher_id): + publisher = self.publishers.find_one({"_id": ObjectId(publisher_id)}) + return publisher + + def insert_entry(self, publisher_name): + print("inserting publisher entry", publisher_name) + + publisher = {"name": publisher_name} + + # now insert the post + try: + result = self.publishers.insert_one(publisher) + print("Matching publisher: ", result.matched_count) + print("Modified publisher: ", result.modified_count) + except: + print("Error inserting publisher") + print("Unexpected error:", sys.exc_info()) + + def update_entry(self, publisher_id, publisher_name): + print("upserting publisher entry", publisher_name) + + filter_doc = {"_id": ObjectId(publisher_id)} + publisher = { "$set": {"name": publisher_name}} + + # now insert the post + try: + result = self.publishers.update_one(filter_doc, publisher) + print("Modified publisher: ", result.modified_count) + except: + print("Error inserting publisher") + print("Unexpected error:", sys.exc_info()) diff --git a/bottle-docker/app/src/comics/storyArcDAO.py b/bottle-docker/app/src/comics/storyArcDAO.py new file mode 100644 index 0000000..afe49a5 --- /dev/null +++ b/bottle-docker/app/src/comics/storyArcDAO.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +from bson.objectid import ObjectId +import sys + + +class StoryArcDAO: + # constructor for the class + def __init__(self, database): + self.db = database + self.storyarcs = database.storyarcs + + def get_storyarcs(self): + cursor = self.storyarcs.find() + l = [] + for storyarc in cursor: + l.append({'title': storyarc['title'], '_id': storyarc['_id']}) + return l + + def get_storyarc(self, storyarc_id): + storyarc = self.storyarcs.find_one({"_id": ObjectId(storyarc_id)}) + return storyarc + + def insert_entry(self, storyarc_title): + print("inserting publisher entry", storyarc_title) + + storyarc = {"name": storyarc_title} + + # now insert the post + try: + result = self.storyarcs.insert_one(storyarc) + print("Matching storyarc: ", result.matched_count) + print("Modified storyarc: ", result.modified_count) + except: + print("Error inserting storyarc") + print("Unexpected error:", sys.exc_info()) + + def update_entry(self, storyarc_id, storyarc_title): + print("upserting storyarc entry", storyarc_title) + + filter_doc = {"_id": ObjectId(storyarc_id)} + storyarc = {"$set": {"name": storyarc_title}} + + # now insert the post + try: + result = self.storyarcs.update_one(filter_doc, storyarc) + print("Modified storyarc: ", result.modified_count) + except: + print("Error inserting storyarc") + print("Unexpected error:", sys.exc_info()) diff --git a/bottle-docker/app/src/kontor.css b/bottle-docker/app/src/kontor.css new file mode 100644 index 0000000..979f80f --- /dev/null +++ b/bottle-docker/app/src/kontor.css @@ -0,0 +1,89 @@ +body { +font-family: sans-serif; +color: #333333; +padding:4em 0 4em; +} +body, +.wrapper { +margin: 10px auto; +/*max-width: 60em;*/ +} + +header, nav, nav a, main, section, footer { +border-radius: 0px 0.5em 0.5em; +border: 1px solid; +padding: 10px; +margin: 10px; +} + +header { +position:fixed; +top:0px; +left:0px; +right:0px; +text-align:center; +padding:10px; +background: lightgrey; +/*border-bottom: 1px solid #d5d5d5;*/ +} + +nav { + position: fixed; + padding-top: 10em; + +font-size: 0.91em; +float: left; +width: 15em; +padding: 0; +background: lightskyblue; +border-color: skyblue; +} + +nav ul { +padding: 0; +} + +nav li { +list-style: none; +margin: 0; +padding: 0.1em; +} + +nav a { +display: block; +padding: 0.2em 10px; +font-weight: bold; +text-decoration: none; +background-color: skyblue; +color: #333; +} + +nav ul a:hover, +nav ul a:active { +color: #fffbf0; +background-color: #dfac20; +} + +main { +display: block; +background: lightblue; +border-color: #8a9da8; +margin-left: 15em; +min-width: 16em; /* Mindestbreite (der Überschrift) verhindert Anzeigefehler in modernen Browsern */ +} + +footer { +position:fixed; +padding: 10px; +margin-top: 10px; +bottom:0; +left: 0; +right:0; +background: lightgrey; +border-color: grey; +} + +footer p { +float:right; +margin: 0; +} diff --git a/bottle-docker/app/src/kontor.py b/bottle-docker/app/src/kontor.py new file mode 100644 index 0000000..e6309bc --- /dev/null +++ b/bottle-docker/app/src/kontor.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +import pymongo +import sessionDAO +import userDAO +import comics +#import library +import bottle +import cgi +import re + + +__author__ = 'tpeetz' + +app = bottle.Bottle() + + +def index(): + cookie = bottle.request.get_cookie("session") + username = sessions.get_username(cookie) + return bottle.template('kontor', dict(username=username)) + + +def show_signup(): + return bottle.template("signup", dict(username="", + password="", + password_error="", + email="", + username_error="", + email_error="", + verify_error ="")) + + +def process_signup(): + email = bottle.request.forms.get("email") + username = bottle.request.forms.get("username") + password = bottle.request.forms.get("password") + verify = bottle.request.forms.get("verify") + + # set these up in case we have an error case + errors = {'username': cgi.escape(username), 'email': cgi.escape(email)} + if validate_signup(username, password, verify, email, errors): + + if not users.add_user(username, password, email): + # this was a duplicate + errors['username_error'] = "Username already in use. Please choose another" + return bottle.template("signup", errors) + + session_id = sessions.start_session(username) + print(session_id) + bottle.response.set_cookie("session", session_id) + bottle.redirect("/welcome") + else: + print("user did not validate") + return bottle.template("signup", errors) + + +def show_login(): + return bottle.template('login', dict(username="", password="", login_error="")) + +def process_login(): + username = bottle.request.forms.get("username") + password = bottle.request.forms.get("password") + + print("user submitted ", username, "pass ", password) + + user_record = users.validate_login(username, password) + if user_record: + # username is stored in the user collection in the _id key + session_id = sessions.start_session(user_record['_id']) + + if session_id is None: + bottle.redirect("/internal_error") + + cookie = session_id + + # Warning, if you are running into a problem whereby the cookie being set here is + # not getting set on the redirect, you are probably using the experimental version of bottle (.12). + # revert to .11 to solve the problem. + bottle.response.set_cookie("session", cookie) + + bottle.redirect("/") + + else: + return bottle.template("login", dict(username=cgi.escape(username), password="", login_error="Invalid Login")) + + +def process_logout(): + cookie = bottle.request.get_cookie("session") + sessions.end_session(cookie) + bottle.response.set_cookie("session", "") + bottle.redirect("/") + + +def send_stylesheet(filename): + return bottle.static_file(filename, root='.', mimetype='text/css') + + +def setup_routing(app): + app.route('/', 'GET', index) + app.route('/signup', 'GET', show_signup) + app.route('/signup', 'POST', process_signup) + app.route('/login', 'GET', show_login) + app.route('/login', 'POST', process_login) + app.route('/logout', 'GET', process_logout) + app.route('/css/', 'GET', send_stylesheet) + + +# validates that the user information is valid for new signup, return True of False +# and fills in the error string if there is an issue +def validate_signup(username, password, verify, email, errors): + USER_RE = re.compile(r"^[a-zA-Z0-9_-]{3,20}$") + PASS_RE = re.compile(r"^.{3,20}$") + EMAIL_RE = re.compile(r"^[\S]+@[\S]+\.[\S]+$") + + errors['username_error'] = "" + errors['password_error'] = "" + errors['verify_error'] = "" + errors['email_error'] = "" + + if not USER_RE.match(username): + errors['username_error'] = "invalid username. try just letters and numbers" + return False + + if not PASS_RE.match(password): + errors['password_error'] = "invalid password." + return False + if password != verify: + errors['verify_error'] = "password must match" + return False + if email != "": + if not EMAIL_RE.match(email): + errors['email_error'] = "invalid email address" + return False + return True + + +setup_routing(app) + +connection = pymongo.MongoClient("mongodb://mongodb") +database = connection.kontor + +users = userDAO.UserDAO(database) +sessions = sessionDAO.SessionDAO(database) + +comics_plugin = comics.Plugin(app, database, sessions) +#library_plugin = library.Plugin(app, database, sessions) +print("starting app Kontor") +bottle.run(app, host="0.0.0.0", port=9000, debug=True) diff --git a/bottle-docker/app/src/sessionDAO.py b/bottle-docker/app/src/sessionDAO.py new file mode 100644 index 0000000..29a09cd --- /dev/null +++ b/bottle-docker/app/src/sessionDAO.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +import sys +import random +import string + + +# The session Data Access Object handles interactions with the sessions collection + +class SessionDAO: + + def __init__(self, database): + self.db = database + self.sessions = database.sessions + + # will start a new session id by adding a new document to the sessions collection + # returns the sessionID or None + def start_session(self, username): + + session_id = self.get_random_str(32) + session = {'username': username, '_id': session_id} + + try: + self.sessions.insert_one(session) + except: + print("Unexpected error on start_session:", sys.exc_info()[0]) + return None + + return str(session['_id']) + + # will send a new user session by deleting from sessions table + def end_session(self, session_id): + + if session_id is None: + return + + self.sessions.delete_one({'_id': session_id}) + + return + + # if there is a valid session, it is returned + def get_session(self, session_id): + + if session_id is None: + return None + + session = self.sessions.find_one({'_id': session_id}) + + return session + + # get the username of the current session, or None if the session is not valid + def get_username(self, session_id): + + session = self.get_session(session_id) + if session is None: + return None + else: + return session['username'] + + def get_random_str(self, num_chars): + random_string = "" + for i in range(num_chars): + random_string = random_string + random.choice(string.ascii_letters) + return random_string diff --git a/bottle-docker/app/src/userDAO.py b/bottle-docker/app/src/userDAO.py new file mode 100644 index 0000000..df5a107 --- /dev/null +++ b/bottle-docker/app/src/userDAO.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +import hmac +import random +import string +import hashlib +import pymongo + + +# The User Data Access Object handles all interactions with the User collection. +class UserDAO: + + def __init__(self, db): + self.db = db + self.users = self.db.users + self.SECRET = 'verysecret' + + # makes a little salt + def make_salt(self): + salt = "" + for i in range(5): + salt = salt + random.choice(string.ascii_letters) + return salt + + # implement the function make_pw_hash(name, pw) that returns a hashed password + # of the format: + # HASH(pw + salt),salt + # use sha256 + + def make_pw_hash(self, pw,salt=None): + if salt == None: + salt = self.make_salt(); + return hashlib.sha256(pw + salt).hexdigest()+","+ salt + + # Validates a user login. Returns user record or None + def validate_login(self, username, password): + + user = None + try: + user = self.users.find_one({'_id': username}) + except: + print("Unable to query database for user") + + if user is None: + print("User not in database") + return None + + salt = user['password'].split(',')[1] + + if user['password'] != self.make_pw_hash(password, salt): + print("user password is not a match") + return None + + # Looks good + return user + + + # creates a new user in the users collection + def add_user(self, username, password, email): + password_hash = self.make_pw_hash(password) + + user = {'_id': username, 'password': password_hash} + if email != "": + user['email'] = email + + try: + self.users.insert_one(user) + except pymongo.errors.OperationFailure: + print("oops, mongo error") + return False + except pymongo.errors.DuplicateKeyError as e: + print("oops, username is already taken") + return False + + return True diff --git a/bottle-docker/app/src/views/artist_list.tpl b/bottle-docker/app/src/views/artist_list.tpl new file mode 100644 index 0000000..366d4b7 --- /dev/null +++ b/bottle-docker/app/src/views/artist_list.tpl @@ -0,0 +1,39 @@ + + + +Kontor + + + +
Kontor
+ +
+
+ +
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle-docker/app/src/views/artist_template.tpl b/bottle-docker/app/src/views/artist_template.tpl new file mode 100644 index 0000000..f981355 --- /dev/null +++ b/bottle-docker/app/src/views/artist_template.tpl @@ -0,0 +1,43 @@ + + + +Kontor + + + +
Kontor
+ +
+
+
+ {{errors}} +

Title

+ +
+

+ +

+
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + + diff --git a/bottle-docker/app/src/views/comic_index.tpl b/bottle-docker/app/src/views/comic_index.tpl new file mode 100644 index 0000000..529a38e --- /dev/null +++ b/bottle-docker/app/src/views/comic_index.tpl @@ -0,0 +1,34 @@ + + + +Kontor + + + +
Kontor
+ +
+
+
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle-docker/app/src/views/comic_list.tpl b/bottle-docker/app/src/views/comic_list.tpl new file mode 100644 index 0000000..a947073 --- /dev/null +++ b/bottle-docker/app/src/views/comic_list.tpl @@ -0,0 +1,44 @@ + + + +Kontor + + + +
Kontor
+ +
+
+ + + +%for comic in comics: + + + +%end + +
TitleCurrent OrderCompleted
{{comic['title']}}{{comic['current_order']}}{{comic['completed']}}
+
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle-docker/app/src/views/comic_template.tpl b/bottle-docker/app/src/views/comic_template.tpl new file mode 100644 index 0000000..38f190e --- /dev/null +++ b/bottle-docker/app/src/views/comic_template.tpl @@ -0,0 +1,51 @@ + + + +Kontor + + + +
Kontor
+ +
+
+
+ {{errors}} +

Title

+ +
+

+ + +

+ +

+
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle-docker/app/src/views/kontor.tpl b/bottle-docker/app/src/views/kontor.tpl new file mode 100644 index 0000000..260cb88 --- /dev/null +++ b/bottle-docker/app/src/views/kontor.tpl @@ -0,0 +1,27 @@ + + + +Kontor + + + +
Kontor
+ +
+
+
+
+%if (username == None): +
Login

Ingenieurbüro Thomas Peetz

+%end +%if (username != None): + +%end + + diff --git a/bottle-docker/app/src/views/login.tpl b/bottle-docker/app/src/views/login.tpl new file mode 100644 index 0000000..881a1af --- /dev/null +++ b/bottle-docker/app/src/views/login.tpl @@ -0,0 +1,52 @@ + + + +Kontor + + + +
Kontor
+ +
+
+
+ + + + + + + + + + + + + +
Username
Password{{login_error}}
+ + +
+
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle-docker/app/src/views/publisher_list.tpl b/bottle-docker/app/src/views/publisher_list.tpl new file mode 100644 index 0000000..7139e17 --- /dev/null +++ b/bottle-docker/app/src/views/publisher_list.tpl @@ -0,0 +1,39 @@ + + + +Kontor + + + +
Kontor
+ +
+
+ +
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle-docker/app/src/views/publisher_template.tpl b/bottle-docker/app/src/views/publisher_template.tpl new file mode 100644 index 0000000..91cbc4a --- /dev/null +++ b/bottle-docker/app/src/views/publisher_template.tpl @@ -0,0 +1,42 @@ + + + +Kontor + + + +
Kontor
+ +
+
+
+ {{errors}} +

Title

+ +
+

+ +

+
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle-docker/app/src/views/signup.tpl b/bottle-docker/app/src/views/signup.tpl new file mode 100644 index 0000000..2b3790c --- /dev/null +++ b/bottle-docker/app/src/views/signup.tpl @@ -0,0 +1,59 @@ + + + +Kontor + + + +
Kontor
+ +
+
+

Signup

+
+ + + + + + + + + + + + + + + + + + + + +
Username{{username_error}}
Password{{password_error}}
Verify Password{{verify_error}}
Email (optional){{email_error}}
+ +
+
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle-docker/app/src/views/storyarc_list.tpl b/bottle-docker/app/src/views/storyarc_list.tpl new file mode 100644 index 0000000..82a5ce3 --- /dev/null +++ b/bottle-docker/app/src/views/storyarc_list.tpl @@ -0,0 +1,40 @@ + + + +Kontor + + + +
Kontor
+ +
+
+ +
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle-docker/app/src/views/storyarc_template.tpl b/bottle-docker/app/src/views/storyarc_template.tpl new file mode 100644 index 0000000..010d885 --- /dev/null +++ b/bottle-docker/app/src/views/storyarc_template.tpl @@ -0,0 +1,43 @@ + + + +Kontor + + + +
Kontor
+ +
+
+
+ {{errors}} +

Title

+ +
+

+ +

+
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle-docker/docker-compose.yaml b/bottle-docker/docker-compose.yaml new file mode 100644 index 0000000..2469e59 --- /dev/null +++ b/bottle-docker/docker-compose.yaml @@ -0,0 +1,31 @@ +services: + mongodb: + image: mongo + restart: always + volumes: + - db-data:/var/lib/mongo + networks: + - backend-network + app: + build: app + restart: always + ports: + - 9000:9000 + networks: + - backend-network + - frontend-network + nginx: + image: nginx + restart: always + volumes: + - ./nginx:/etc/nginx/conf.d + - ./front-end:/var/www/front-end + ports: + - 8070:80 + networks: + - frontend-network +volumes: + db-data: +networks: + backend-network: + frontend-network: diff --git a/bottle-docker/front-end/index.html b/bottle-docker/front-end/index.html new file mode 100644 index 0000000..a9a660b --- /dev/null +++ b/bottle-docker/front-end/index.html @@ -0,0 +1,82 @@ + + + + Hello! + + + + + + +
+ +

Hello there!

+

Simple DEV environment setup with Docker and Docker Compose

+
+

Set url path(default is '/'), then query the app service.

+
+ + +
+

Server response

+
+ +
+
+
+ + + + + + diff --git a/bottle-docker/nginx/nginx.conf b/bottle-docker/nginx/nginx.conf new file mode 100644 index 0000000..7be3c45 --- /dev/null +++ b/bottle-docker/nginx/nginx.conf @@ -0,0 +1,14 @@ +server { + listen 80; + server_name localhost; + root /var/www/front-end; + + location /api { + proxy_pass http://app:9000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + From cbc57d22f88f47221257454590d4e80d19a2fd27 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 30 Mar 2025 04:17:58 +0200 Subject: [PATCH 90/91] add Docker build and domcker-compose file --- springboot/Dockerfile | 5 ++ springboot/docker-compose.yml | 32 +++++++++++ springboot/src/main/resources/application.yml | 56 ++++++++++++------- 3 files changed, 73 insertions(+), 20 deletions(-) create mode 100644 springboot/Dockerfile create mode 100644 springboot/docker-compose.yml diff --git a/springboot/Dockerfile b/springboot/Dockerfile new file mode 100644 index 0000000..1ac7138 --- /dev/null +++ b/springboot/Dockerfile @@ -0,0 +1,5 @@ +FROM alpine/java:21-jdk +WORKDIR / +ADD build/libs/kontor-spring-0.1.0-SNAPSHOT.jar app.jar +EXPOSE 8000 +CMD java -jar -Dspring.profiles.active=prod -Dvaadin.productionMode=true app.jar diff --git a/springboot/docker-compose.yml b/springboot/docker-compose.yml new file mode 100644 index 0000000..0adf164 --- /dev/null +++ b/springboot/docker-compose.yml @@ -0,0 +1,32 @@ + +services: + mariadb: + image: mariadb + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: kontor + MYSQL_USER: kontor + MYSQL_PASSWORD: kontor + MYSQL_DATABASE: kontor + ports: + - 3316:3306 + networks: + - back-end + volumes: + - mariadb-storage:/var/lib/mysql:rw + kontor: + image: kontor + restart: unless-stopped + networks: + - back-end + - front-end + ports: + - 8000:8000 + +networks: + back-end: + front-end: + +volumes: + mariadb-storage: + diff --git a/springboot/src/main/resources/application.yml b/springboot/src/main/resources/application.yml index fd8dfff..696dd47 100644 --- a/springboot/src/main/resources/application.yml +++ b/springboot/src/main/resources/application.yml @@ -1,5 +1,3 @@ -server: - port: 8085 app: name: 'Kontor' shortName: 'Kontor' @@ -7,24 +5,6 @@ app: spring: profiles: active: local,dev,test,prod - devtools: - add-properties: false - datasource: - driverClassName: org.mariadb.jdbc.Driver - url: jdbc:mariadb://localhost:3306/kontor - username: 'kontor' - password: 'kontor' - #driverClassName: org.hsqldb.jdbc.JDBCDriver - #url: jdbc:hsqldb:file:kontorHSQLDB - #username: 'sa' - #password: 'sa' - #jpa - #database-platform: org.hibernate.community.dialect.SQLiteDialect - #datasource - #driverClassName: org.sqlite.JDBC - #url: "jdbc:sqlite:file:./kontorDb?cache=shared" - #username=sa - #password=sa jpa: defer-datasource-initialization: true #hibernate.ddl-auto=create-drop @@ -65,3 +45,39 @@ mail: userName: 'thomas.peetz@thpeetz.de' password: 'fS9f4JYDIO7A' starttls: true +--- +spring: + config: + activate: + on-profile: prod + datasource: + driverClassName: org.mariadb.jdbc.Driver + url: jdbc:mariadb://mariadb:3306/kontor + username: 'kontor' + password: 'kontor' +server: + port: 8000 +--- +spring: + config: + activate: + on-profile: local, dev, test + devtools: + add-properties: false + datasource: + driverClassName: org.mariadb.jdbc.Driver + url: jdbc:mariadb://localhost:3306/kontor + username: 'kontor' + password: 'kontor' + #driverClassName: org.hsqldb.jdbc.JDBCDriver + #url: jdbc:hsqldb:file:kontorHSQLDB + #username: 'sa' + #password: 'sa' + #driverClassName: org.sqlite.JDBC + #url: "jdbc:sqlite:file:./kontorDb?cache=shared" + #username=sa + #password=sa + #jpa + #database-platform: org.hibernate.community.dialect.SQLiteDialect +server: + port: 8085 From fd5bc54eeec3dc78bf9dc9b25890cc6e31802e10 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 30 Mar 2025 15:57:09 +0200 Subject: [PATCH 91/91] import repositories kontor-java and kontor-ee --- java-ee/ComicsImpl/build.gradle | 5 + .../config/checkstyle/checkstyle.xml | 192 +++++++++ .../config/checkstyle/checkstyle.xsl | 179 ++++++++ .../ComicsImpl/config/findbugs/findbugs.xml | 15 + .../java/com/peetz/comics/dal/ArtistDao.java | 19 + .../java/com/peetz/comics/dal/ArtistImpl.java | 45 ++ .../java/com/peetz/comics/dal/ComicDao.java | 23 ++ .../java/com/peetz/comics/dal/ComicImpl.java | 62 +++ .../com/peetz/comics/entity/ArtistEntity.java | 69 ++++ .../com/peetz/comics/entity/ComicEntity.java | 90 ++++ .../com/peetz/comics/entity/IssueEntity.java | 80 ++++ .../peetz/comics/entity/PublisherEntity.java | 48 +++ .../peetz/comics/entity/StoryArcEntity.java | 56 +++ .../com/peetz/comics/entity/VolumeEntity.java | 37 ++ .../peetz/comics/service/ComicService.java | 34 ++ .../comics/service/ComicServiceImpl.java | 92 +++++ .../peetz/comics/service/package-info.java | 8 + .../comics/service/ComicServiceImplTest.java | 195 +++++++++ java-ee/ComicsWeb/build.gradle | 7 + .../java/com/peetz/comics/view/ComicView.java | 36 ++ .../ComicsWeb/src/main/webapp/artistAdd.jsp | 64 +++ .../ComicsWeb/src/main/webapp/artistEdit.jsp | 66 +++ .../ComicsWeb/src/main/webapp/artistList.jsp | 95 +++++ .../ComicsWeb/src/main/webapp/comicAdd.jsp | 67 +++ .../ComicsWeb/src/main/webapp/comicEdit.jsp | 90 ++++ .../ComicsWeb/src/main/webapp/comicList.jsp | 99 +++++ .../ComicsWeb/src/main/webapp/comics.xhtml | 51 +++ java-ee/ComicsWeb/src/main/webapp/index.jsp | 35 ++ .../ComicsWeb/src/main/webapp/issueAdd.jsp | 67 +++ .../ComicsWeb/src/main/webapp/issueEdit.jsp | 66 +++ .../src/main/webapp/publisherAdd.jsp | 64 +++ .../src/main/webapp/publisherEdit.jsp | 66 +++ .../src/main/webapp/publisherList.jsp | 95 +++++ java-ee/DVDs.csv | 391 ++++++++++++++++++ java-ee/Jenkinsfile | 15 + java-ee/KontorApp/build.gradle | 19 + .../main/java/com/ibtp/kontor/KontorApp.java | 23 ++ .../main/java/com/ibtp/kontor/KontorGUI.java | 69 ++++ .../com/ibtp/kontor/comics/dal/ArtistDao.java | 26 ++ .../ibtp/kontor/comics/dal/ArtistImpl.java | 67 +++ .../com/ibtp/kontor/comics/dal/ComicDao.java | 26 ++ .../com/ibtp/kontor/comics/dal/ComicImpl.java | 65 +++ .../com/ibtp/kontor/comics/dal/IssueDao.java | 23 ++ .../com/ibtp/kontor/comics/dal/IssueImpl.java | 57 +++ .../ibtp/kontor/comics/dal/PublisherDao.java | 26 ++ .../ibtp/kontor/comics/dal/PublisherImpl.java | 62 +++ .../ibtp/kontor/comics/dal/StoryArcDao.java | 24 ++ .../ibtp/kontor/comics/dal/StoryArcImpl.java | 55 +++ .../com/ibtp/kontor/comics/dal/VolumeDao.java | 24 ++ .../ibtp/kontor/comics/dal/VolumeImpl.java | 57 +++ .../kontor/comics/entity/ArtistEntity.java | 72 ++++ .../kontor/comics/entity/ComicEntity.java | 81 ++++ .../kontor/comics/entity/IssueEntity.java | 75 ++++ .../kontor/comics/entity/PublisherEntity.java | 47 +++ .../kontor/comics/entity/StoryArcEntity.java | 48 +++ .../kontor/comics/entity/VolumeEntity.java | 40 ++ .../ibtp/kontor/comics/view/ComicsMenu.java | 13 + .../java/com/ibtp/kontor/dal/BaseImpl.java | 21 + .../java/com/ibtp/kontor/dal/Database.java | 11 + .../com/ibtp/kontor/dal/DatabaseManager.java | 23 ++ .../com/ibtp/kontor/dal/LocalDatabase.java | 103 +++++ .../ibtp/kontor/library/dal/ArticleDao.java | 23 ++ .../ibtp/kontor/library/dal/ArticleImpl.java | 64 +++ .../ibtp/kontor/library/dal/AuthorDao.java | 25 ++ .../ibtp/kontor/library/dal/AuthorImpl.java | 65 +++ .../com/ibtp/kontor/library/dal/BookDao.java | 22 + .../com/ibtp/kontor/library/dal/BookImpl.java | 38 ++ .../com/ibtp/kontor/library/dal/FileDao.java | 22 + .../com/ibtp/kontor/library/dal/FileImpl.java | 38 ++ .../com/ibtp/kontor/library/dal/TitleDao.java | 22 + .../ibtp/kontor/library/dal/TitleImpl.java | 38 ++ .../kontor/library/entity/ArticleEntity.java | 56 +++ .../kontor/library/entity/AuthorEntity.java | 62 +++ .../kontor/library/entity/BookEntity.java | 96 +++++ .../kontor/library/entity/FileEntity.java | 41 ++ .../kontor/library/entity/TitleEntity.java | 27 ++ .../ibtp/kontor/library/view/LibraryMenu.java | 13 + .../kontor/tradingcards/dal/BaseSetDao.java | 22 + .../kontor/tradingcards/dal/BaseSetImpl.java | 51 +++ .../kontor/tradingcards/dal/InsertDao.java | 22 + .../kontor/tradingcards/dal/InsertImpl.java | 38 ++ .../tradingcards/dal/ManufacturerDao.java | 29 ++ .../tradingcards/dal/ManufacturerImpl.java | 65 +++ .../tradingcards/dal/ParallelSetDao.java | 22 + .../tradingcards/dal/ParallelSetImpl.java | 38 ++ .../kontor/tradingcards/dal/PlayerDao.java | 24 ++ .../kontor/tradingcards/dal/PlayerImpl.java | 43 ++ .../kontor/tradingcards/dal/PositionDao.java | 26 ++ .../kontor/tradingcards/dal/PositionImpl.java | 64 +++ .../kontor/tradingcards/dal/SportCardDao.java | 22 + .../tradingcards/dal/SportCardImpl.java | 38 ++ .../kontor/tradingcards/dal/SportDao.java | 26 ++ .../kontor/tradingcards/dal/SportImpl.java | 64 +++ .../ibtp/kontor/tradingcards/dal/TeamDao.java | 26 ++ .../kontor/tradingcards/dal/TeamImpl.java | 66 +++ .../tradingcards/entity/BaseSetEntity.java | 67 +++ .../tradingcards/entity/InsertEntity.java | 74 ++++ .../entity/ManufacturerEntity.java | 78 ++++ .../entity/ParallelSetEntity.java | 53 +++ .../tradingcards/entity/PlayerEntity.java | 46 +++ .../tradingcards/entity/PositionEntity.java | 56 +++ .../tradingcards/entity/SportCardEntity.java | 68 +++ .../tradingcards/entity/SportEntity.java | 75 ++++ .../tradingcards/entity/TeamEntity.java | 48 +++ .../tradingcards/view/TradingCardsMenu.java | 13 + .../main/resources/META-INF/persistence.xml | 39 ++ .../KontorApp/src/main/resources/logback.xml | 40 ++ .../ibtp/kontor/comics/CollectionTest.java | 41 ++ .../kontor/comics/dal/ArtistImplTest.java | 50 +++ .../ibtp/kontor/comics/dal/ComicImplTest.java | 54 +++ .../ibtp/kontor/comics/dal/IssueImplTest.java | 62 +++ .../kontor/comics/dal/PublisherImplTest.java | 50 +++ .../kontor/comics/dal/StoryArcImplTest.java | 55 +++ .../kontor/comics/dal/VolumeImplTest.java | 67 +++ .../ibtp/kontor/dal/DataAccessLayerTest.java | 62 +++ .../ibtp/kontor/library/BookshelfTest.java | 22 + .../kontor/library/dal/ArticleImplTest.java | 41 ++ .../kontor/library/dal/AuthorImplTest.java | 52 +++ .../ibtp/kontor/library/dal/BookImplTest.java | 27 ++ .../ibtp/kontor/library/dal/FileImplTest.java | 27 ++ .../kontor/library/dal/TitleImplTest.java | 27 ++ .../kontor/tradingcards/CollectionTest.java | 167 ++++++++ .../tradingcards/dal/BaseSetImplTest.java | 27 ++ .../tradingcards/dal/InsertImplTest.java | 28 ++ .../dal/ManufacturerImplTest.java | 52 +++ .../tradingcards/dal/ParallelSetImplTest.java | 27 ++ .../tradingcards/dal/PlayerImplTest.java | 27 ++ .../tradingcards/dal/PositionImplTest.java | 42 ++ .../tradingcards/dal/SportCardImplTest.java | 27 ++ .../tradingcards/dal/SportImplTest.java | 41 ++ .../kontor/tradingcards/dal/TeamImplTest.java | 41 ++ .../ibtp/kontor/util/LocalTestDatabase.java | 110 +++++ .../test/resources/META-INF/persistence.xml | 39 ++ .../KontorApp/src/test/resources/logback.xml | 41 ++ java-ee/KontorEJB/build.gradle | 12 + .../java/com/ibtp/kontor/ejb/Controller.java | 62 +++ .../java/com/ibtp/kontor/ejb/Property.java | 37 ++ .../com/ibtp/kontor/ejb/PropertyManager.java | 28 ++ java-ee/KontorImpl/build.gradle | 5 + .../config/checkstyle/checkstyle.xml | 192 +++++++++ .../config/checkstyle/checkstyle.xsl | 179 ++++++++ .../KontorImpl/config/findbugs/findbugs.xml | 15 + .../com/peetz/kontor/dal/KontorUserDao.java | 16 + .../com/peetz/kontor/dal/KontorUserImpl.java | 53 +++ .../peetz/kontor/entity/KontorUserEntity.java | 62 +++ .../peetz/kontor/service/package-info.java | 8 + java-ee/KontorWeb/build.gradle | 11 + .../com/peetz/kontor/data/ExportComics.java | 88 ++++ .../com/peetz/kontor/data/ExportLibrary.java | 100 +++++ .../com/peetz/kontor/data/ExportMedien.java | 75 ++++ .../peetz/kontor/data/ExportTradingCards.java | 175 ++++++++ .../com/peetz/kontor/data/FileExport.java | 64 +++ .../com/peetz/kontor/data/FileImport.java | 72 ++++ .../com/peetz/kontor/data/ImportComics.java | 81 ++++ .../com/peetz/kontor/data/ImportLibrary.java | 88 ++++ .../com/peetz/kontor/data/ImportMedien.java | 91 ++++ .../peetz/kontor/data/ImportTradingCards.java | 52 +++ .../main/resources/META-INF/persistence.xml | 51 +++ .../src/main/webapp/WEB-INF/faces-config.xml | 77 ++++ .../src/main/webapp/WEB-INF/glassfish-web.xml | 7 + .../KontorWeb/src/main/webapp/WEB-INF/web.xml | 22 + .../KontorWeb/src/main/webapp/comics.xhtml | 39 ++ .../KontorWeb/src/main/webapp/css/store.css | 145 +++++++ java-ee/KontorWeb/src/main/webapp/index.xhtml | 48 +++ .../src/main/webapp/kontorTemplate.xhtml | 44 ++ .../KontorWeb/src/main/webapp/library.xhtml | 39 ++ .../KontorWeb/src/main/webapp/medien.xhtml | 39 ++ .../main/webapp/resources/css/cssLayout.css | 71 ++++ .../src/main/webapp/resources/css/default.css | 29 ++ java-ee/KontorWeb/src/main/webapp/seite.html | 17 + java-ee/KontorWeb/src/main/webapp/sport.xhtml | 40 ++ .../src/main/webapp/sport/sportAdd.xhtml | 39 ++ .../src/main/webapp/sport/sportDetails.xhtml | 39 ++ .../src/main/webapp/tradingcards.xhtml | 40 ++ java-ee/LibraryImpl/build.gradle | 5 + .../config/checkstyle/checkstyle.xml | 192 +++++++++ .../config/checkstyle/checkstyle.xsl | 179 ++++++++ .../LibraryImpl/config/findbugs/findbugs.xml | 15 + .../com/peetz/library/dal/ArticleDao.java | 25 ++ .../java/com/peetz/library/dal/BookDao.java | 15 + .../com/peetz/library/dal/BookshelfDao.java | 22 + .../java/com/peetz/library/dal/FileDao.java | 15 + .../com/peetz/library/dal/MagazineDao.java | 15 + .../com/peetz/library/dal/ShelfObjectDao.java | 15 + .../com/peetz/library/dal/ShelfboardDao.java | 15 + .../com/peetz/library/dal/package-info.java | 8 + .../peetz/library/entity/ArticleEntity.java | 87 ++++ .../com/peetz/library/entity/BookEntity.java | 93 +++++ .../peetz/library/entity/BookshelfEntity.java | 53 +++ .../com/peetz/library/entity/FileEntity.java | 49 +++ .../peetz/library/entity/MagazineEntity.java | 49 +++ .../library/entity/ShelfObjectEntity.java | 32 ++ .../library/entity/ShelfboardEntity.java | 57 +++ .../peetz/library/entity/package-info.java | 8 + .../peetz/library/service/LibraryService.java | 28 ++ .../library/service/LibraryServiceImpl.java | 64 +++ .../peetz/library/service/package-info.java | 8 + java-ee/LibraryWeb/build.gradle | 7 + .../com/peetz/library/view/LibraryView.java | 42 ++ java-ee/LibraryWeb/src/main/webapp/index.jsp | 33 ++ .../src/main/webapp/jsp/articleAdd.jsp | 67 +++ .../src/main/webapp/jsp/articleEdit.jsp | 69 ++++ .../src/main/webapp/jsp/articleList.jsp | 90 ++++ .../src/main/webapp/jsp/boardAdd.jsp | 68 +++ .../src/main/webapp/jsp/boardEdit.jsp | 67 +++ .../src/main/webapp/jsp/bookAdd.jsp | 67 +++ .../src/main/webapp/jsp/bookEdit.jsp | 85 ++++ .../src/main/webapp/jsp/bookList.jsp | 33 ++ .../LibraryWeb/src/main/webapp/jsp/index.jsp | 58 +++ .../src/main/webapp/jsp/shelfAdd.jsp | 67 +++ .../src/main/webapp/jsp/shelfEdit.jsp | 92 +++++ .../src/main/webapp/jsp/shelfList.jsp | 91 ++++ java-ee/MedienImpl/build.gradle | 5 + .../config/checkstyle/checkstyle.xml | 192 +++++++++ .../config/checkstyle/checkstyle.xsl | 179 ++++++++ .../MedienImpl/config/findbugs/findbugs.xml | 15 + .../com/peetz/medien/dal/package-info.java | 8 + .../peetz/medien/entity/AudioCDEntity.java | 91 ++++ .../com/peetz/medien/entity/BoxSetEntity.java | 43 ++ .../com/peetz/medien/entity/FilmEntity.java | 50 +++ .../com/peetz/medien/entity/package-info.java | 8 + .../peetz/medien/service/MedienService.java | 22 + .../medien/service/MedienServiceImpl.java | 75 ++++ .../peetz/medien/service/package-info.java | 8 + java-ee/MedienWeb/build.gradle | 7 + .../com/peetz/medien/view/MedienView.java | 36 ++ java-ee/MedienWeb/src/main/webapp/index.jsp | 33 ++ .../MedienWeb/src/main/webapp/jsp/cdAdd.jsp | 75 ++++ .../MedienWeb/src/main/webapp/jsp/cdEdit.jsp | 75 ++++ .../MedienWeb/src/main/webapp/jsp/cdList.jsp | 94 +++++ .../MedienWeb/src/main/webapp/jsp/dvdAdd.jsp | 67 +++ .../MedienWeb/src/main/webapp/jsp/dvdEdit.jsp | 69 ++++ .../MedienWeb/src/main/webapp/jsp/dvdList.jsp | 90 ++++ .../MedienWeb/src/main/webapp/jsp/index.jsp | 58 +++ java-ee/README.md | 2 + java-ee/TradingCardsImpl/build.gradle | 5 + .../config/checkstyle/checkstyle.xml | 192 +++++++++ .../config/checkstyle/checkstyle.xsl | 179 ++++++++ .../config/findbugs/findbugs.xml | 15 + .../tradingcards/dal/ManufacturerDao.java | 31 ++ .../tradingcards/dal/ManufacturerImpl.java | 67 +++ .../com/peetz/tradingcards/dal/SportDao.java | 28 ++ .../com/peetz/tradingcards/dal/SportImpl.java | 58 +++ .../tradingcards/entity/BaseSetEntity.java | 67 +++ .../tradingcards/entity/InsertEntity.java | 74 ++++ .../entity/ManufacturerEntity.java | 74 ++++ .../entity/ParallelSetEntity.java | 53 +++ .../tradingcards/entity/PlayerEntity.java | 46 +++ .../tradingcards/entity/PositionEntity.java | 53 +++ .../tradingcards/entity/SportCardEntity.java | 68 +++ .../tradingcards/entity/SportEntity.java | 64 +++ .../peetz/tradingcards/entity/TeamEntity.java | 42 ++ .../tradingcards/service/SportService.java | 24 ++ .../service/SportServiceImpl.java | 63 +++ .../service/TradingcardService.java | 28 ++ .../service/TradingcardServiceImpl.java | 75 ++++ .../tradingcards/service/package-info.java | 5 + .../dal/ManufacturerImplTest.java | 147 +++++++ java-ee/TradingCardsWeb/build.gradle | 7 + .../peetz/tradingcards/view/SportView.java | 52 +++ .../tradingcards/view/TradingCardsView.java | 36 ++ .../TradingCardsWeb/src/main/webapp/index.jsp | 33 ++ java-ee/build.gradle | 69 ++++ java-ee/comics.xml | 63 +++ java-ee/config/checkstyle/checkstyle.xml | 192 +++++++++ java-ee/config/checkstyle/checkstyle.xsl | 179 ++++++++ java-ee/config/findbugs/findbugs.xml | 15 + java-ee/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54208 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + java-ee/gradlew | 172 ++++++++ java-ee/gradlew.bat | 84 ++++ java-ee/settings.gradle | 13 + java/README.md | 2 + java/build.gradle | 87 ++++ java/comics.xml | 63 +++ java/config/checkstyle/checkstyle.xml | 192 +++++++++ java/config/checkstyle/checkstyle.xsl | 179 ++++++++ java/config/findbugs/findbugs.xml | 15 + java/gradle.properties | 2 + java/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes java/gradle/wrapper/gradle-wrapper.properties | 5 + java/gradlew | 185 +++++++++ java/gradlew.bat | 89 ++++ java/settings.gradle | 1 + .../main/java/com/ibtp/kontor/Database.java | 46 +++ .../main/java/com/ibtp/kontor/DumpComics.java | 51 +++ .../main/java/com/ibtp/kontor/KontorApp.java | 23 ++ .../main/java/com/ibtp/kontor/KontorGUI.java | 69 ++++ .../com/ibtp/kontor/comics/dal/ArtistDao.java | 26 ++ .../ibtp/kontor/comics/dal/ArtistImpl.java | 68 +++ .../com/ibtp/kontor/comics/dal/ComicDao.java | 26 ++ .../com/ibtp/kontor/comics/dal/ComicImpl.java | 65 +++ .../com/ibtp/kontor/comics/dal/IssueDao.java | 23 ++ .../com/ibtp/kontor/comics/dal/IssueImpl.java | 57 +++ .../ibtp/kontor/comics/dal/PublisherDao.java | 26 ++ .../ibtp/kontor/comics/dal/PublisherImpl.java | 62 +++ .../ibtp/kontor/comics/dal/StoryArcDao.java | 24 ++ .../ibtp/kontor/comics/dal/StoryArcImpl.java | 55 +++ .../com/ibtp/kontor/comics/dal/VolumeDao.java | 24 ++ .../ibtp/kontor/comics/dal/VolumeImpl.java | 57 +++ .../com/ibtp/kontor/comics/entity/Artist.java | 46 +++ .../kontor/comics/entity/ArtistEntity.java | 72 ++++ .../com/ibtp/kontor/comics/entity/Comic.java | 98 +++++ .../kontor/comics/entity/ComicEntity.java | 81 ++++ .../com/ibtp/kontor/comics/entity/Issue.java | 66 +++ .../kontor/comics/entity/IssueEntity.java | 75 ++++ .../ibtp/kontor/comics/entity/Publisher.java | 60 +++ .../kontor/comics/entity/PublisherEntity.java | 47 +++ .../ibtp/kontor/comics/entity/StoryArc.java | 59 +++ .../kontor/comics/entity/StoryArcEntity.java | 48 +++ .../kontor/comics/entity/TradePaperback.java | 56 +++ .../com/ibtp/kontor/comics/entity/Volume.java | 70 ++++ .../kontor/comics/entity/VolumeEntity.java | 40 ++ .../ibtp/kontor/comics/view/ComicsMenu.java | 13 + .../java/com/ibtp/kontor/dal/BaseImpl.java | 21 + .../java/com/ibtp/kontor/dal/Database.java | 11 + .../com/ibtp/kontor/dal/DatabaseManager.java | 23 ++ .../com/ibtp/kontor/dal/LocalDatabase.java | 104 +++++ .../ibtp/kontor/library/dal/ArticleDao.java | 23 ++ .../ibtp/kontor/library/dal/ArticleImpl.java | 64 +++ .../ibtp/kontor/library/dal/AuthorDao.java | 25 ++ .../ibtp/kontor/library/dal/AuthorImpl.java | 65 +++ .../com/ibtp/kontor/library/dal/BookDao.java | 22 + .../com/ibtp/kontor/library/dal/BookImpl.java | 38 ++ .../com/ibtp/kontor/library/dal/FileDao.java | 22 + .../com/ibtp/kontor/library/dal/FileImpl.java | 38 ++ .../com/ibtp/kontor/library/dal/TitleDao.java | 22 + .../ibtp/kontor/library/dal/TitleImpl.java | 38 ++ .../kontor/library/entity/ArticleEntity.java | 56 +++ .../kontor/library/entity/AuthorEntity.java | 62 +++ .../kontor/library/entity/BookEntity.java | 96 +++++ .../kontor/library/entity/FileEntity.java | 41 ++ .../kontor/library/entity/TitleEntity.java | 27 ++ .../ibtp/kontor/library/view/LibraryMenu.java | 13 + .../kontor/tradingcards/dal/BaseSetDao.java | 22 + .../kontor/tradingcards/dal/BaseSetImpl.java | 51 +++ .../kontor/tradingcards/dal/InsertDao.java | 22 + .../kontor/tradingcards/dal/InsertImpl.java | 38 ++ .../tradingcards/dal/ManufacturerDao.java | 29 ++ .../tradingcards/dal/ManufacturerImpl.java | 65 +++ .../tradingcards/dal/ParallelSetDao.java | 22 + .../tradingcards/dal/ParallelSetImpl.java | 38 ++ .../kontor/tradingcards/dal/PlayerDao.java | 24 ++ .../kontor/tradingcards/dal/PlayerImpl.java | 43 ++ .../kontor/tradingcards/dal/PositionDao.java | 26 ++ .../kontor/tradingcards/dal/PositionImpl.java | 65 +++ .../kontor/tradingcards/dal/SportCardDao.java | 22 + .../tradingcards/dal/SportCardImpl.java | 38 ++ .../kontor/tradingcards/dal/SportDao.java | 26 ++ .../kontor/tradingcards/dal/SportImpl.java | 64 +++ .../ibtp/kontor/tradingcards/dal/TeamDao.java | 26 ++ .../kontor/tradingcards/dal/TeamImpl.java | 66 +++ .../tradingcards/entity/BaseSetEntity.java | 67 +++ .../tradingcards/entity/InsertEntity.java | 74 ++++ .../entity/ManufacturerEntity.java | 78 ++++ .../entity/ParallelSetEntity.java | 53 +++ .../tradingcards/entity/PlayerEntity.java | 46 +++ .../tradingcards/entity/PositionEntity.java | 66 +++ .../tradingcards/entity/SportCardEntity.java | 68 +++ .../tradingcards/entity/SportEntity.java | 75 ++++ .../tradingcards/entity/TeamEntity.java | 48 +++ .../tradingcards/view/TradingCardsMenu.java | 13 + .../main/resources/META-INF/persistence.xml | 39 ++ java/src/main/resources/logback.xml | 40 ++ .../ibtp/kontor/comics/CollectionTest.java | 43 ++ .../kontor/comics/dal/ArtistImplTest.java | 65 +++ .../ibtp/kontor/comics/dal/ComicImplTest.java | 57 +++ .../ibtp/kontor/comics/dal/IssueImplTest.java | 64 +++ .../kontor/comics/dal/PublisherImplTest.java | 56 +++ .../kontor/comics/dal/StoryArcImplTest.java | 63 +++ .../kontor/comics/dal/VolumeImplTest.java | 72 ++++ .../ibtp/kontor/dal/DataAccessLayerTest.java | 63 +++ .../ibtp/kontor/library/BookshelfTest.java | 23 ++ .../kontor/library/dal/ArticleImplTest.java | 57 +++ .../kontor/library/dal/AuthorImplTest.java | 56 +++ .../ibtp/kontor/library/dal/BookImplTest.java | 28 ++ .../ibtp/kontor/library/dal/FileImplTest.java | 28 ++ .../kontor/library/dal/TitleImplTest.java | 28 ++ .../kontor/tradingcards/CollectionTest.java | 168 ++++++++ .../tradingcards/dal/BaseSetImplTest.java | 28 ++ .../tradingcards/dal/InsertImplTest.java | 29 ++ .../dal/ManufacturerImplTest.java | 57 +++ .../tradingcards/dal/ParallelSetImplTest.java | 28 ++ .../tradingcards/dal/PlayerImplTest.java | 28 ++ .../tradingcards/dal/PositionImplTest.java | 44 ++ .../tradingcards/dal/SportCardImplTest.java | 28 ++ .../tradingcards/dal/SportImplTest.java | 44 ++ .../kontor/tradingcards/dal/TeamImplTest.java | 44 ++ .../ibtp/kontor/util/LocalTestDatabase.java | 110 +++++ .../test/resources/META-INF/persistence.xml | 39 ++ java/src/test/resources/logback.xml | 41 ++ java/tysc-20041010-1819.sql | 168 ++++++++ springboot/docker-compose.yml | 10 +- 393 files changed, 21181 insertions(+), 5 deletions(-) create mode 100644 java-ee/ComicsImpl/build.gradle create mode 100644 java-ee/ComicsImpl/config/checkstyle/checkstyle.xml create mode 100644 java-ee/ComicsImpl/config/checkstyle/checkstyle.xsl create mode 100644 java-ee/ComicsImpl/config/findbugs/findbugs.xml create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ArtistDao.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ArtistImpl.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ComicDao.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ComicImpl.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/ArtistEntity.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/ComicEntity.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/IssueEntity.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/PublisherEntity.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/StoryArcEntity.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/VolumeEntity.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/ComicService.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/ComicServiceImpl.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/package-info.java create mode 100644 java-ee/ComicsImpl/src/test/java/com/peetz/comics/service/ComicServiceImplTest.java create mode 100644 java-ee/ComicsWeb/build.gradle create mode 100644 java-ee/ComicsWeb/src/main/java/com/peetz/comics/view/ComicView.java create mode 100644 java-ee/ComicsWeb/src/main/webapp/artistAdd.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/artistEdit.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/artistList.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/comicAdd.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/comicEdit.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/comicList.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/comics.xhtml create mode 100644 java-ee/ComicsWeb/src/main/webapp/index.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/issueAdd.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/issueEdit.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/publisherAdd.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/publisherEdit.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/publisherList.jsp create mode 100644 java-ee/DVDs.csv create mode 100644 java-ee/Jenkinsfile create mode 100644 java-ee/KontorApp/build.gradle create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/KontorApp.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/KontorGUI.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ArtistDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ArtistImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ComicDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ComicImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/IssueDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/IssueImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/PublisherDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/PublisherImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/StoryArcDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/StoryArcImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/VolumeDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/VolumeImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/ArtistEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/ComicEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/IssueEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/PublisherEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/StoryArcEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/VolumeEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/view/ComicsMenu.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/BaseImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/Database.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/DatabaseManager.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/LocalDatabase.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/ArticleDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/ArticleImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/AuthorDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/AuthorImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/BookDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/BookImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/FileDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/FileImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/TitleDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/TitleImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/ArticleEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/AuthorEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/BookEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/FileEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/TitleEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/view/LibraryMenu.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/BaseSetEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/InsertEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/ManufacturerEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/ParallelSetEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/PlayerEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/PositionEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/SportCardEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/SportEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/TeamEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/view/TradingCardsMenu.java create mode 100644 java-ee/KontorApp/src/main/resources/META-INF/persistence.xml create mode 100644 java-ee/KontorApp/src/main/resources/logback.xml create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/CollectionTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/ArtistImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/ComicImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/IssueImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/PublisherImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/StoryArcImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/VolumeImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/dal/DataAccessLayerTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/BookshelfTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/ArticleImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/AuthorImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/BookImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/FileImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/TitleImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/CollectionTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/BaseSetImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/InsertImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/PlayerImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/PositionImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/SportCardImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/SportImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/TeamImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/util/LocalTestDatabase.java create mode 100644 java-ee/KontorApp/src/test/resources/META-INF/persistence.xml create mode 100644 java-ee/KontorApp/src/test/resources/logback.xml create mode 100755 java-ee/KontorEJB/build.gradle create mode 100755 java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/Controller.java create mode 100755 java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/Property.java create mode 100755 java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/PropertyManager.java create mode 100644 java-ee/KontorImpl/build.gradle create mode 100644 java-ee/KontorImpl/config/checkstyle/checkstyle.xml create mode 100644 java-ee/KontorImpl/config/checkstyle/checkstyle.xsl create mode 100644 java-ee/KontorImpl/config/findbugs/findbugs.xml create mode 100644 java-ee/KontorImpl/src/main/java/com/peetz/kontor/dal/KontorUserDao.java create mode 100644 java-ee/KontorImpl/src/main/java/com/peetz/kontor/dal/KontorUserImpl.java create mode 100644 java-ee/KontorImpl/src/main/java/com/peetz/kontor/entity/KontorUserEntity.java create mode 100644 java-ee/KontorImpl/src/main/java/com/peetz/kontor/service/package-info.java create mode 100755 java-ee/KontorWeb/build.gradle create mode 100644 java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportComics.java create mode 100644 java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportLibrary.java create mode 100644 java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportMedien.java create mode 100644 java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportTradingCards.java create mode 100644 java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/FileExport.java create mode 100644 java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/FileImport.java create mode 100644 java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ImportComics.java create mode 100644 java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ImportLibrary.java create mode 100644 java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ImportMedien.java create mode 100644 java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ImportTradingCards.java create mode 100755 java-ee/KontorWeb/src/main/resources/META-INF/persistence.xml create mode 100644 java-ee/KontorWeb/src/main/webapp/WEB-INF/faces-config.xml create mode 100644 java-ee/KontorWeb/src/main/webapp/WEB-INF/glassfish-web.xml create mode 100644 java-ee/KontorWeb/src/main/webapp/WEB-INF/web.xml create mode 100644 java-ee/KontorWeb/src/main/webapp/comics.xhtml create mode 100755 java-ee/KontorWeb/src/main/webapp/css/store.css create mode 100755 java-ee/KontorWeb/src/main/webapp/index.xhtml create mode 100644 java-ee/KontorWeb/src/main/webapp/kontorTemplate.xhtml create mode 100644 java-ee/KontorWeb/src/main/webapp/library.xhtml create mode 100644 java-ee/KontorWeb/src/main/webapp/medien.xhtml create mode 100644 java-ee/KontorWeb/src/main/webapp/resources/css/cssLayout.css create mode 100644 java-ee/KontorWeb/src/main/webapp/resources/css/default.css create mode 100644 java-ee/KontorWeb/src/main/webapp/seite.html create mode 100644 java-ee/KontorWeb/src/main/webapp/sport.xhtml create mode 100644 java-ee/KontorWeb/src/main/webapp/sport/sportAdd.xhtml create mode 100644 java-ee/KontorWeb/src/main/webapp/sport/sportDetails.xhtml create mode 100644 java-ee/KontorWeb/src/main/webapp/tradingcards.xhtml create mode 100644 java-ee/LibraryImpl/build.gradle create mode 100644 java-ee/LibraryImpl/config/checkstyle/checkstyle.xml create mode 100644 java-ee/LibraryImpl/config/checkstyle/checkstyle.xsl create mode 100644 java-ee/LibraryImpl/config/findbugs/findbugs.xml create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ArticleDao.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/BookDao.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/BookshelfDao.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/FileDao.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/MagazineDao.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ShelfObjectDao.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ShelfboardDao.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/package-info.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ArticleEntity.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/BookEntity.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/BookshelfEntity.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/FileEntity.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/MagazineEntity.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ShelfObjectEntity.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ShelfboardEntity.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/package-info.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/service/LibraryService.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/service/LibraryServiceImpl.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/service/package-info.java create mode 100644 java-ee/LibraryWeb/build.gradle create mode 100644 java-ee/LibraryWeb/src/main/java/com/peetz/library/view/LibraryView.java create mode 100644 java-ee/LibraryWeb/src/main/webapp/index.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/articleAdd.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/articleEdit.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/articleList.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/boardAdd.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/boardEdit.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/bookAdd.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/bookEdit.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/bookList.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/index.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/shelfAdd.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/shelfEdit.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/shelfList.jsp create mode 100644 java-ee/MedienImpl/build.gradle create mode 100644 java-ee/MedienImpl/config/checkstyle/checkstyle.xml create mode 100644 java-ee/MedienImpl/config/checkstyle/checkstyle.xsl create mode 100644 java-ee/MedienImpl/config/findbugs/findbugs.xml create mode 100644 java-ee/MedienImpl/src/main/java/com/peetz/medien/dal/package-info.java create mode 100644 java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/AudioCDEntity.java create mode 100644 java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/BoxSetEntity.java create mode 100644 java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/FilmEntity.java create mode 100644 java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/package-info.java create mode 100644 java-ee/MedienImpl/src/main/java/com/peetz/medien/service/MedienService.java create mode 100644 java-ee/MedienImpl/src/main/java/com/peetz/medien/service/MedienServiceImpl.java create mode 100644 java-ee/MedienImpl/src/main/java/com/peetz/medien/service/package-info.java create mode 100644 java-ee/MedienWeb/build.gradle create mode 100644 java-ee/MedienWeb/src/main/java/com/peetz/medien/view/MedienView.java create mode 100644 java-ee/MedienWeb/src/main/webapp/index.jsp create mode 100644 java-ee/MedienWeb/src/main/webapp/jsp/cdAdd.jsp create mode 100644 java-ee/MedienWeb/src/main/webapp/jsp/cdEdit.jsp create mode 100644 java-ee/MedienWeb/src/main/webapp/jsp/cdList.jsp create mode 100644 java-ee/MedienWeb/src/main/webapp/jsp/dvdAdd.jsp create mode 100644 java-ee/MedienWeb/src/main/webapp/jsp/dvdEdit.jsp create mode 100644 java-ee/MedienWeb/src/main/webapp/jsp/dvdList.jsp create mode 100644 java-ee/MedienWeb/src/main/webapp/jsp/index.jsp create mode 100644 java-ee/README.md create mode 100644 java-ee/TradingCardsImpl/build.gradle create mode 100644 java-ee/TradingCardsImpl/config/checkstyle/checkstyle.xml create mode 100644 java-ee/TradingCardsImpl/config/checkstyle/checkstyle.xsl create mode 100644 java-ee/TradingCardsImpl/config/findbugs/findbugs.xml create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/ManufacturerDao.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/ManufacturerImpl.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/SportDao.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/SportImpl.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/BaseSetEntity.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/InsertEntity.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/ManufacturerEntity.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/ParallelSetEntity.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/PlayerEntity.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/PositionEntity.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/SportCardEntity.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/SportEntity.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/TeamEntity.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/SportService.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/SportServiceImpl.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/TradingcardService.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/TradingcardServiceImpl.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/package-info.java create mode 100644 java-ee/TradingCardsImpl/src/test/java/com/peetz/tradingcards/dal/ManufacturerImplTest.java create mode 100644 java-ee/TradingCardsWeb/build.gradle create mode 100644 java-ee/TradingCardsWeb/src/main/java/com/peetz/tradingcards/view/SportView.java create mode 100644 java-ee/TradingCardsWeb/src/main/java/com/peetz/tradingcards/view/TradingCardsView.java create mode 100644 java-ee/TradingCardsWeb/src/main/webapp/index.jsp create mode 100644 java-ee/build.gradle create mode 100644 java-ee/comics.xml create mode 100644 java-ee/config/checkstyle/checkstyle.xml create mode 100644 java-ee/config/checkstyle/checkstyle.xsl create mode 100644 java-ee/config/findbugs/findbugs.xml create mode 100644 java-ee/gradle/wrapper/gradle-wrapper.jar create mode 100644 java-ee/gradle/wrapper/gradle-wrapper.properties create mode 100755 java-ee/gradlew create mode 100755 java-ee/gradlew.bat create mode 100755 java-ee/settings.gradle create mode 100644 java/README.md create mode 100644 java/build.gradle create mode 100644 java/comics.xml create mode 100644 java/config/checkstyle/checkstyle.xml create mode 100644 java/config/checkstyle/checkstyle.xsl create mode 100644 java/config/findbugs/findbugs.xml create mode 100644 java/gradle.properties create mode 100644 java/gradle/wrapper/gradle-wrapper.jar create mode 100644 java/gradle/wrapper/gradle-wrapper.properties create mode 100755 java/gradlew create mode 100644 java/gradlew.bat create mode 100644 java/settings.gradle create mode 100644 java/src/main/java/com/ibtp/kontor/Database.java create mode 100644 java/src/main/java/com/ibtp/kontor/DumpComics.java create mode 100644 java/src/main/java/com/ibtp/kontor/KontorApp.java create mode 100644 java/src/main/java/com/ibtp/kontor/KontorGUI.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/ArtistDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/ArtistImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/ComicDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/ComicImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/IssueDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/IssueImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/PublisherDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/PublisherImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/StoryArcDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/StoryArcImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/VolumeDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/VolumeImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/Artist.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/ArtistEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/Comic.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/ComicEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/Issue.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/IssueEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/Publisher.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/PublisherEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/StoryArc.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/StoryArcEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/TradePaperback.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/Volume.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/VolumeEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/view/ComicsMenu.java create mode 100644 java/src/main/java/com/ibtp/kontor/dal/BaseImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/dal/Database.java create mode 100644 java/src/main/java/com/ibtp/kontor/dal/DatabaseManager.java create mode 100644 java/src/main/java/com/ibtp/kontor/dal/LocalDatabase.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/dal/ArticleDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/dal/ArticleImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/dal/AuthorDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/dal/AuthorImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/dal/BookDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/dal/BookImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/dal/FileDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/dal/FileImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/dal/TitleDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/dal/TitleImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/entity/ArticleEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/entity/AuthorEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/entity/BookEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/entity/FileEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/entity/TitleEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/view/LibraryMenu.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/entity/BaseSetEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/entity/InsertEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/entity/ManufacturerEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/entity/ParallelSetEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/entity/PlayerEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/entity/PositionEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/entity/SportCardEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/entity/SportEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/entity/TeamEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/view/TradingCardsMenu.java create mode 100644 java/src/main/resources/META-INF/persistence.xml create mode 100644 java/src/main/resources/logback.xml create mode 100644 java/src/test/java/com/ibtp/kontor/comics/CollectionTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/comics/dal/ArtistImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/comics/dal/ComicImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/comics/dal/IssueImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/comics/dal/PublisherImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/comics/dal/StoryArcImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/comics/dal/VolumeImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/dal/DataAccessLayerTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/library/BookshelfTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/library/dal/ArticleImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/library/dal/AuthorImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/library/dal/BookImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/library/dal/FileImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/library/dal/TitleImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/tradingcards/CollectionTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/tradingcards/dal/BaseSetImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/tradingcards/dal/InsertImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/tradingcards/dal/PlayerImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/tradingcards/dal/PositionImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/tradingcards/dal/SportCardImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/tradingcards/dal/SportImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/tradingcards/dal/TeamImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/util/LocalTestDatabase.java create mode 100644 java/src/test/resources/META-INF/persistence.xml create mode 100644 java/src/test/resources/logback.xml create mode 100644 java/tysc-20041010-1819.sql diff --git a/java-ee/ComicsImpl/build.gradle b/java-ee/ComicsImpl/build.gradle new file mode 100644 index 0000000..9aa2749 --- /dev/null +++ b/java-ee/ComicsImpl/build.gradle @@ -0,0 +1,5 @@ +jar { + manifest { + attributes 'Implementation-Title': 'Comics', 'Implementation-Version': version + } +} diff --git a/java-ee/ComicsImpl/config/checkstyle/checkstyle.xml b/java-ee/ComicsImpl/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..7c682c3 --- /dev/null +++ b/java-ee/ComicsImpl/config/checkstyle/checkstyle.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java-ee/ComicsImpl/config/checkstyle/checkstyle.xsl b/java-ee/ComicsImpl/config/checkstyle/checkstyle.xsl new file mode 100644 index 0000000..393a01b --- /dev/null +++ b/java-ee/ComicsImpl/config/checkstyle/checkstyle.xsl @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

CheckStyle Audit

Designed for use with CheckStyle and Ant.
+
+ + + +
+ + + +
+ + + + + +

+

+ +


+ + + + +
+ + + + +

Files

+ + + + + + + + + + + + + + +
NameErrors
+
+ + + + +

File

+ + + + + + + + + + + + + +
Error DescriptionLine
+ Back to top +
+ + + +

Summary

+ + + + + + + + + + + + +
FilesErrors
+
+ + + + a + b + + +
+ + diff --git a/java-ee/ComicsImpl/config/findbugs/findbugs.xml b/java-ee/ComicsImpl/config/findbugs/findbugs.xml new file mode 100644 index 0000000..34a6e01 --- /dev/null +++ b/java-ee/ComicsImpl/config/findbugs/findbugs.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ArtistDao.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ArtistDao.java new file mode 100644 index 0000000..346f363 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ArtistDao.java @@ -0,0 +1,19 @@ +package com.peetz.comics.dal; + +import java.util.List; + +import javax.ejb.Local; + +import com.peetz.comics.entity.ArtistEntity; + +@Local +public interface ArtistDao { + + public ArtistEntity getById(Long id); + + public List findByIds(List ids); + + public ArtistEntity store(ArtistEntity entity); + + public void delete(ArtistEntity entity); +} \ No newline at end of file diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ArtistImpl.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ArtistImpl.java new file mode 100644 index 0000000..0f18e6c --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ArtistImpl.java @@ -0,0 +1,45 @@ +package com.peetz.comics.dal; + +import java.util.List; + +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +import com.peetz.comics.entity.ArtistEntity; + +@Stateless(name = "ArtistDao") +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class ArtistImpl implements ArtistDao { + + @PersistenceContext(unitName = "kontor") + private EntityManager em; + + @Override + public ArtistEntity getById(Long id) { + Query q = em.createNamedQuery(""); + q.setParameter("id", id); + ArtistEntity entity = (ArtistEntity)q.getSingleResult(); + return entity; + } + + @Override + public List findByIds(List ids) { + // TODO Auto-generated method stub + return null; + } + + @Override + public ArtistEntity store(ArtistEntity entity) { + em.persist(entity); + return entity; + } + + @Override + public void delete(ArtistEntity entity) { + em.remove(entity); + } +} \ No newline at end of file diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ComicDao.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ComicDao.java new file mode 100644 index 0000000..957bc01 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ComicDao.java @@ -0,0 +1,23 @@ +package com.peetz.comics.dal; + +import java.util.List; + +import javax.ejb.Local; + +import com.peetz.comics.entity.ComicEntity; +import com.peetz.comics.entity.PublisherEntity; + +@Local +public interface ComicDao { + public ComicEntity getById(Long id); + + public List findByIds(List ids); + + public List findByTitle(String title); + + public ComicEntity assignPublisher(ComicEntity comic, PublisherEntity publisher); + + public ComicEntity store(ComicEntity entity); + + public void delete(ComicEntity entity); +} diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ComicImpl.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ComicImpl.java new file mode 100644 index 0000000..d2adb99 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ComicImpl.java @@ -0,0 +1,62 @@ +package com.peetz.comics.dal; + +import java.util.List; + +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +import com.peetz.comics.entity.ComicEntity; +import com.peetz.comics.entity.PublisherEntity; + +@Stateless(name = "ComicDao") +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class ComicImpl implements ComicDao { + + @PersistenceContext(unitName = "kontor") + private EntityManager em; + + @Override + public ComicEntity getById(Long id) { + Query q = em.createNamedQuery("Comic.findById"); + q.setParameter("id", id); + ComicEntity entity = (ComicEntity)q.getSingleResult(); + return entity; + } + + @Override + public List findByIds(List ids) { + // TODO Auto-generated method stub + return null; + } + + @Override + public List findByTitle(String title) { + Query q = em.createNamedQuery("Comic.findByTitle"); + q.setParameter("title", title); + @SuppressWarnings("unchecked") + List resultList = q.getResultList(); + return resultList; + } + + @Override + public ComicEntity assignPublisher(ComicEntity comic, + PublisherEntity publisher) { + // TODO Auto-generated method stub + return null; + } + + @Override + public ComicEntity store(ComicEntity entity) { + em.persist(entity); + return entity; + } + + @Override + public void delete(ComicEntity entity) { + em.remove(entity); + } +} diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/ArtistEntity.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/ArtistEntity.java new file mode 100644 index 0000000..57e0474 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/ArtistEntity.java @@ -0,0 +1,69 @@ +package com.peetz.comics.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Artist.findAll", query="SELECT a from ArtistEntity as a"), + @NamedQuery(name="Artist.findByName", query="SELECT a from ArtistEntity as a WHERE a.name = :name") +}) + +@Entity +@Table(name="ARTIST") +public class ArtistEntity { + + private Long id; + + private String name; + + private Collection writtenIssues = new ArrayList(); + + + private Collection inkedIssues = new ArrayList(); + + private Collection penciledIssues = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { return name; } + + public void setName(String name) { this.name = name; } + + public void setWrittenIssues(Collection writtenIssues) { this.writtenIssues = writtenIssues; } + + @OneToMany(mappedBy="writer", cascade=CascadeType.REMOVE) + public Collection getWrittenIssues() { + return writtenIssues; + } + + public void setInkedIssues(Collection inkedIssues) { this.inkedIssues = inkedIssues; } + + @OneToMany(mappedBy="inker", cascade=CascadeType.REMOVE) + public Collection getInkedIssues() { + return inkedIssues; + } + + public void setPenciledIssues(Collection penciledIssues) { this.penciledIssues = penciledIssues; } + + @OneToMany(mappedBy="penciler", cascade=CascadeType.REMOVE) + public Collection getPenciledIssues() { + return penciledIssues; + } +} diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/ComicEntity.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/ComicEntity.java new file mode 100644 index 0000000..4475647 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/ComicEntity.java @@ -0,0 +1,90 @@ +package com.peetz.comics.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Comic.findAll", query="SELECT c from ComicEntity as c"), + @NamedQuery(name="Comic.findByTitle", query="SELECT c from ComicEntity as c WHERE c.title = :title") +}) + +@Entity +@Table(name="COMIC") +public class ComicEntity +{ + private Long id; + + private String title; + + private Boolean completed; + + private Boolean currentOrder; + + private Collection issues = new ArrayList(); + + private Collection storyArc = new ArrayList(); + + private Collection volumes = new ArrayList(); + + public PublisherEntity publisher; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + @Column + public Boolean getCompleted() { return completed; } + + public Boolean isCompleted() { return completed; } + + public void setCompleted(Boolean completed) { this.completed = completed; } + + @Column + public Boolean getCurrentOrder() { return currentOrder; } + + public Boolean isCurrentOrder() { return currentOrder; } + + public void setCurrentOrder(Boolean currentOrder) { this.currentOrder = currentOrder; } + + public void setIssues(Collection issues) { this.issues = issues; } + + @OneToMany(mappedBy="comic", cascade=CascadeType.REMOVE) + public Collection getIssues() { return issues; } + + public void setStoryArc(Collection storyArc) { this.storyArc = storyArc; } + + @OneToMany(mappedBy="comic", cascade=CascadeType.REMOVE) + public Collection getStoryArc() { return storyArc; } + + public void setVolumes(Collection volumes) { this.volumes = volumes; } + + @OneToMany(mappedBy="comic", cascade=CascadeType.REMOVE) + public Collection getVolumes() { return volumes; } + + @ManyToOne + public PublisherEntity getPublisher() { return publisher; } + + public void setPublisher(PublisherEntity publisher) { + this.publisher = publisher; + } +} diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/IssueEntity.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/IssueEntity.java new file mode 100644 index 0000000..fbda2c4 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/IssueEntity.java @@ -0,0 +1,80 @@ +package com.peetz.comics.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Issue.findAll", query="SELECT i from IssueEntity as i"), + @NamedQuery(name="Issue.findByNumber", query="SELECT i from IssueEntity as i WHERE i.number = :number") +}) + +@Entity +@Table(name = "ISSUE") +public class IssueEntity { + + private Long id; + + private String number; + + private Boolean completed; + + private ComicEntity comic; + + private ArtistEntity writer; + + private ArtistEntity inker; + + private ArtistEntity penciler; + + private StoryArcEntity storyArc; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getNumber() { return number; } + + public void setNumber(String number) { this.number = number; } + + @Column + public Boolean getCompleted() { return completed; } + public Boolean isCompleted() { return completed; } + + public void setCompleted(Boolean completed) { this.completed = completed; } + + public void setComic(ComicEntity comic) { this.comic = comic; } + + @ManyToOne + public ComicEntity getComic() { return comic; } + + public void setWriter(ArtistEntity writer) { this.writer = writer; } + + @ManyToOne + public ArtistEntity getWriter() { return writer; } + + public void setInker(ArtistEntity inker) { this.inker = inker; } + + @ManyToOne + public ArtistEntity getInker() { return inker; } + + public void setPenciler(ArtistEntity penciler) { this.penciler = penciler; } + + @ManyToOne + public ArtistEntity getPenciler() { return penciler; } + + public void setStoryArc(StoryArcEntity storyArc) { this.storyArc = storyArc; } + + @ManyToOne + public StoryArcEntity getStoryArc() { return storyArc; } +} diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/PublisherEntity.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/PublisherEntity.java new file mode 100644 index 0000000..9252ea8 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/PublisherEntity.java @@ -0,0 +1,48 @@ +package com.peetz.comics.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Publisher.findAll", query="SELECT p from PublisherEntity as p"), + @NamedQuery(name="Publisher.findByName", query="SELECT p from PublisherEntity as p WHERE p.name = :name") +}) + +@Entity +@Table(name="PUBLISHER") +public class PublisherEntity { + + private Long id; + + private String name; + + private Collection comic = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { return name; } + + public void setName(String name) { this.name = name; } + + public void setComic(Collection comic) { this.comic = comic; } + + @OneToMany(mappedBy="publisher", cascade=CascadeType.REMOVE) + public Collection getComic() { return comic; } +} diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/StoryArcEntity.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/StoryArcEntity.java new file mode 100644 index 0000000..cffec70 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/StoryArcEntity.java @@ -0,0 +1,56 @@ +package com.peetz.comics.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="StoryArc.findAll", query="SELECT a from StoryArcEntity as a"), + @NamedQuery(name="StoryArc.findByArtist", query="SELECT a from StoryArcEntity as a WHERE a.title = :title") +}) + +@Entity +@Table(name="STORYARC") +public class StoryArcEntity { + + private Long id; + + private String title; + + private Collection issues = new ArrayList(); + + private ComicEntity comic; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + public void setIssues(Collection issues) { this.issues = issues; } + + @OneToMany(mappedBy="storyArc", cascade=CascadeType.REMOVE) + public Collection getIssues() { return issues; } + + public void setComic(ComicEntity comic) { this.comic = comic; } + + @ManyToOne + public ComicEntity getComic() { return comic; } +} diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/VolumeEntity.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/VolumeEntity.java new file mode 100644 index 0000000..8f25d13 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/VolumeEntity.java @@ -0,0 +1,37 @@ +package com.peetz.comics.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name="VOLUME") +public class VolumeEntity { + + private Long id; + + private String title; + + ComicEntity comic; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + @ManyToOne + public ComicEntity getComic() { return comic; } + + public void setComic(ComicEntity comic) { this.comic = comic; } +} diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/ComicService.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/ComicService.java new file mode 100644 index 0000000..523d230 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/ComicService.java @@ -0,0 +1,34 @@ +package com.peetz.comics.service; + +import java.util.Collection; + +import javax.ejb.Local; + +import com.peetz.comics.entity.ArtistEntity; +import com.peetz.comics.entity.ComicEntity; +import com.peetz.comics.entity.IssueEntity; +import com.peetz.comics.entity.PublisherEntity; +import com.peetz.comics.entity.StoryArcEntity; + +@Local +public interface ComicService { + public Collection getAllComics(); + + public Collection getAllPublisher(); + + public Collection getAllArtists(); + + public Collection getAllIssuesForComic(ComicEntity comic); + + public Collection getAllStoryArcs(); + + public void addStoryArc(String title); + + public void addPublisher(String name); + + public ComicEntity addComic(String title); + + public PublisherEntity getPublisherById(String nodeValue); + + public void assignPublisher(ComicEntity comic, PublisherEntity publisher); +} diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/ComicServiceImpl.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/ComicServiceImpl.java new file mode 100644 index 0000000..5f3e222 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/ComicServiceImpl.java @@ -0,0 +1,92 @@ +package com.peetz.comics.service; + +import java.util.Collection; + +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; + +import com.peetz.comics.entity.ArtistEntity; +import com.peetz.comics.entity.ComicEntity; +import com.peetz.comics.entity.IssueEntity; +import com.peetz.comics.entity.PublisherEntity; +import com.peetz.comics.entity.StoryArcEntity; +import java.util.ArrayList; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +@Stateless(name="ComicService") +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class ComicServiceImpl implements ComicService { + + @PersistenceContext(unitName = "kontor") + private EntityManager em; + + @SuppressWarnings("unchecked") + @Override + public Collection getAllComics() { + Query query = em.createNamedQuery("Comic.findAll"); + ArrayList comicList = new ArrayList(query.getResultList()); + return comicList; + } + + @Override + public Collection getAllPublisher() { + Query query = em.createNamedQuery("Publisher.findAll"); + @SuppressWarnings("unchecked") + ArrayList publisherList = new ArrayList(query.getResultList()); + return publisherList; + } + + @SuppressWarnings("unchecked") + @Override + public Collection getAllArtists() { + Query query = em.createNamedQuery("Artist.findAll"); + ArrayList artistList = new ArrayList(query.getResultList()); + return artistList; + } + + @Override + public Collection getAllIssuesForComic(ComicEntity comic) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Collection getAllStoryArcs() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void addStoryArc(String title) { + // TODO Auto-generated method stub + + } + + @Override + public void addPublisher(String name) { + // TODO Auto-generated method stub + + } + + @Override + public ComicEntity addComic(String title) { + // TODO Auto-generated method stub + return null; + } + + @Override + public PublisherEntity getPublisherById(String nodeValue) { + // TODO Auto-generated method stub + return null; + } + + @Override + public void assignPublisher(ComicEntity comic, PublisherEntity publisher) { + // TODO Auto-generated method stub + + } + +} diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/package-info.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/package-info.java new file mode 100644 index 0000000..a2d9305 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author TPEETZ + * + */ +package com.peetz.comics.service; \ No newline at end of file diff --git a/java-ee/ComicsImpl/src/test/java/com/peetz/comics/service/ComicServiceImplTest.java b/java-ee/ComicsImpl/src/test/java/com/peetz/comics/service/ComicServiceImplTest.java new file mode 100644 index 0000000..05f38c1 --- /dev/null +++ b/java-ee/ComicsImpl/src/test/java/com/peetz/comics/service/ComicServiceImplTest.java @@ -0,0 +1,195 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package com.peetz.comics.service; + +import com.peetz.comics.entity.ArtistEntity; +import com.peetz.comics.entity.ComicEntity; +import com.peetz.comics.entity.IssueEntity; +import com.peetz.comics.entity.PublisherEntity; +import com.peetz.comics.entity.StoryArcEntity; +import java.util.Collection; +import javax.ejb.embeddable.EJBContainer; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Ignore; + +/** + * + * @author TPEETZ + */ +public class ComicServiceImplTest { + + private static ComicService instance; + private static EJBContainer container; + + public ComicServiceImplTest() { + } + + @BeforeClass + public static void setUpClass() throws Exception { + container = javax.ejb.embeddable.EJBContainer.createEJBContainer(); + instance = (ComicService)container.getContext().lookup("java:global/main/ComicService"); + } + + @AfterClass + public static void tearDownClass() { + container.close(); + } + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + /** + * Test of getAllComics method, of class ComicServiceImpl. + */ + @Ignore + @Test + public void testGetAllComics() throws Exception { + System.out.println("getAllComics"); + Collection expResult = null; + Collection result = instance.getAllComics(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of getAllPublisher method, of class ComicServiceImpl. + */ + @Ignore + @Test + public void testGetAllPublisher() throws Exception { + System.out.println("getAllPublisher"); + Collection expResult = null; + Collection result = instance.getAllPublisher(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of getAllArtists method, of class ComicServiceImpl. + */ + @Ignore + @Test + public void testGetAllArtists() throws Exception { + System.out.println("getAllArtists"); + Collection expResult = null; + Collection result = instance.getAllArtists(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of getAllIssuesForComic method, of class ComicServiceImpl. + */ + @Ignore + @Test + public void testGetAllIssuesForComic() throws Exception { + System.out.println("getAllIssuesForComic"); + ComicEntity comic = null; + Collection expResult = null; + Collection result = instance.getAllIssuesForComic(comic); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of getAllStoryArcs method, of class ComicServiceImpl. + */ + @Ignore + @Test + public void testGetAllStoryArcs() throws Exception { + System.out.println("getAllStoryArcs"); + Collection expResult = null; + Collection result = instance.getAllStoryArcs(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of addStoryArc method, of class ComicServiceImpl. + */ + @Ignore + @Test + public void testAddStoryArc() throws Exception { + System.out.println("addStoryArc"); + String title = ""; + instance.addStoryArc(title); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of addPublisher method, of class ComicServiceImpl. + */ + @Ignore + @Test + public void testAddPublisher() throws Exception { + System.out.println("addPublisher"); + String name = ""; + instance.addPublisher(name); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of addComic method, of class ComicServiceImpl. + */ + @Ignore + @Test + public void testAddComic() throws Exception { + System.out.println("addComic"); + String title = ""; + ComicEntity expResult = null; + ComicEntity result = instance.addComic(title); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of getPublisherById method, of class ComicServiceImpl. + */ + @Ignore + @Test + public void testGetPublisherById() throws Exception { + System.out.println("getPublisherById"); + String nodeValue = ""; + PublisherEntity expResult = null; + PublisherEntity result = instance.getPublisherById(nodeValue); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of assignPublisher method, of class ComicServiceImpl. + */ + @Ignore + @Test + public void testAssignPublisher() throws Exception { + System.out.println("assignPublisher"); + ComicEntity comic = null; + PublisherEntity publisher = null; + instance.assignPublisher(comic, publisher); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +} diff --git a/java-ee/ComicsWeb/build.gradle b/java-ee/ComicsWeb/build.gradle new file mode 100644 index 0000000..30e1d2e --- /dev/null +++ b/java-ee/ComicsWeb/build.gradle @@ -0,0 +1,7 @@ +apply plugin: 'war' + +version = '0.0.1' + +dependencies { + compile project(':ComicsImpl') +} diff --git a/java-ee/ComicsWeb/src/main/java/com/peetz/comics/view/ComicView.java b/java-ee/ComicsWeb/src/main/java/com/peetz/comics/view/ComicView.java new file mode 100644 index 0000000..d21494c --- /dev/null +++ b/java-ee/ComicsWeb/src/main/java/com/peetz/comics/view/ComicView.java @@ -0,0 +1,36 @@ +package com.peetz.comics.view; + +import com.peetz.comics.service.ComicService; +import java.io.Serializable; +import java.util.logging.Logger; +import javax.ejb.EJB; +import javax.faces.bean.ManagedBean; +import javax.faces.bean.RequestScoped; + +/** + * + * @author TPEETZ + */ +@ManagedBean(name="ComicView") +@RequestScoped +public class ComicView implements Serializable { + + private static final Logger LOG = Logger.getLogger(ComicView.class.getName()); + + @EJB + private ComicService comicService; + + private static final long serialVersionUID = -8261128991042235283L; + + public ComicView() { + LOG.info("ComicView created"); + } + + public Integer getComicsNumber() { + return comicService.getAllComics().size(); + } + + public Integer getPublisherNumber() { + return comicService.getAllPublisher().size(); + } +} diff --git a/java-ee/ComicsWeb/src/main/webapp/artistAdd.jsp b/java-ee/ComicsWeb/src/main/webapp/artistAdd.jsp new file mode 100644 index 0000000..9328510 --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/artistAdd.jsp @@ -0,0 +1,64 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + +
Name:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/ComicsWeb/src/main/webapp/artistEdit.jsp b/java-ee/ComicsWeb/src/main/webapp/artistEdit.jsp new file mode 100644 index 0000000..3007a9a --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/artistEdit.jsp @@ -0,0 +1,66 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + +
Name:
+ <%-- hidden fields for id and userId --%> + + <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/ComicsWeb/src/main/webapp/artistList.jsp b/java-ee/ComicsWeb/src/main/webapp/artistList.jsp new file mode 100644 index 0000000..1414873 --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/artistList.jsp @@ -0,0 +1,95 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Liste der Comics
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + +

Comic Manager

+ Show the comic artist list + + + + <%-- set the header --%> + + + + + + <%-- check if publisher exists and display message or iterate over books --%> + + + + + + + + + <%-- print out the book informations --%> + + <%-- print out the edit and delete link for each artist --%> + + + + + + <%-- end interate --%> + + <%-- if publishers cannot be found display a text --%> + + + + + + + +
Artist name  
No artists available
EditDelete
No artists found.
+
+ <%-- add and back to menu button --%> + Add a new artist +   + Back to menu + + + + + +
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/ComicsWeb/src/main/webapp/comicAdd.jsp b/java-ee/ComicsWeb/src/main/webapp/comicAdd.jsp new file mode 100644 index 0000000..c623224 --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/comicAdd.jsp @@ -0,0 +1,67 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Title:
Publisher:
Completed:
Current Order:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/ComicsWeb/src/main/webapp/comicEdit.jsp b/java-ee/ComicsWeb/src/main/webapp/comicEdit.jsp new file mode 100644 index 0000000..ea1cae9 --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/comicEdit.jsp @@ -0,0 +1,90 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Title:
Publisher:
Completed:
Current Order:
+ <%-- hidden fields for id and userId --%> + + <%-- set the parameter for the dispatch action --%> + + + + + + + + + + + + + + + + + + + + +
Issues  
No issues available
EditDelete
No issues available
Add issue
+
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/ComicsWeb/src/main/webapp/comicList.jsp b/java-ee/ComicsWeb/src/main/webapp/comicList.jsp new file mode 100644 index 0000000..4f2cc06 --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/comicList.jsp @@ -0,0 +1,99 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Liste der Comics
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + +

Comic Manager

+ Show the comic list + + + + <%-- set the header --%> + + + + + + + + <%-- check if book exists and display message or iterate over books --%> + + + + + + + + + <%-- print out the book informations --%> + + + + <%-- print out the edit and delete link for each book --%> + + + + + + <%-- end interate --%> + + <%-- if books cannot be found display a text --%> + + + + + + + +
Comic namePublisherOrder  
No comics available
EditDelete
No comicss found.
+
+ <%-- add and back to menu button --%> + Add a new comic +   + Back to menu + + + + + +
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/ComicsWeb/src/main/webapp/comics.xhtml b/java-ee/ComicsWeb/src/main/webapp/comics.xhtml new file mode 100644 index 0000000..506f71f --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/comics.xhtml @@ -0,0 +1,51 @@ + + + + + + + + + Comics Application + + + + + + + + + + + + + + + + + + + + + + + + + +
Kontor Manager
+ Kontor
+ Comics
+ Library
+ Medien
+ TradingCards +
+

Kontor Manager

+ + +
 
+

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/java-ee/ComicsWeb/src/main/webapp/index.jsp b/java-ee/ComicsWeb/src/main/webapp/index.jsp new file mode 100644 index 0000000..5825223 --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/index.jsp @@ -0,0 +1,35 @@ + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + + +
Comic Manager
test +

Comic Manager

+ Show the comic list +
 
+

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/java-ee/ComicsWeb/src/main/webapp/issueAdd.jsp b/java-ee/ComicsWeb/src/main/webapp/issueAdd.jsp new file mode 100644 index 0000000..91aaae1 --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/issueAdd.jsp @@ -0,0 +1,67 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Comic:
Number:
Author:
Read:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/ComicsWeb/src/main/webapp/issueEdit.jsp b/java-ee/ComicsWeb/src/main/webapp/issueEdit.jsp new file mode 100644 index 0000000..0ea25cb --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/issueEdit.jsp @@ -0,0 +1,66 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + +
Number:
Author:
Read:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/ComicsWeb/src/main/webapp/publisherAdd.jsp b/java-ee/ComicsWeb/src/main/webapp/publisherAdd.jsp new file mode 100644 index 0000000..85f220a --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/publisherAdd.jsp @@ -0,0 +1,64 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + +
Name:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/ComicsWeb/src/main/webapp/publisherEdit.jsp b/java-ee/ComicsWeb/src/main/webapp/publisherEdit.jsp new file mode 100644 index 0000000..a8eb56d --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/publisherEdit.jsp @@ -0,0 +1,66 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + +
Name:
+ <%-- hidden fields for id and userId --%> + + <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/ComicsWeb/src/main/webapp/publisherList.jsp b/java-ee/ComicsWeb/src/main/webapp/publisherList.jsp new file mode 100644 index 0000000..b0ef596 --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/publisherList.jsp @@ -0,0 +1,95 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Liste der Comics
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + +

Comic Manager

+ Show the comic list + + + + <%-- set the header --%> + + + + + + <%-- check if publisher exists and display message or iterate over books --%> + + + + + + + + + <%-- print out the book informations --%> + + <%-- print out the edit and delete link for each book --%> + + + + + + <%-- end interate --%> + + <%-- if publishers cannot be found display a text --%> + + + + + + + +
Publisher name  
No publishers available
EditDelete
No publishers found.
+
+ <%-- add and back to menu button --%> + Add a new publisher +   + Back to menu + + + + + +
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/DVDs.csv b/java-ee/DVDs.csv new file mode 100644 index 0000000..1b8df87 --- /dev/null +++ b/java-ee/DVDs.csv @@ -0,0 +1,391 @@ +Title +(T)Raumschiff Surprise - Periode 1 +7 Zwerge - Männer allein im Wald +A Beatiful Mind +About Schmidt +Akte X: Der Film +Aladdin +"Alias - Die Agentin +Vol. 1" +"Alias - Die Agentin +Vol. 2" +"Alias - Die Agentin +Vol. 3" +Alles tanzt nach meiner Pfeife +American Beauty +American History X +American Pie +American Pie 2 +American Pie Jetzt wird geheiratet +American Psycho +An jedem verdammten Sonntag +Angeklagt +Antz +Apollo 13 +Arac Attack +Arachnophobia +Arlington Road +Asso +Atlantis Das Geheimnis der verlorenen Stadt +Austin Powers +Austin Powers Goldständer +Austin Powers Spion in geheimer Missionarsstellung +"Avanti, Avanti" +Backdraft +Bad Taste +Bang Boom Bang +Bärenbrüder +Being John Malkovich +"Berlin, Berlin - Staffel 1" +"Berlin, Berlin - Staffel 2" +"Berlin, Berlin - Staffel 3" +"Berlin, Berlin - Staffel 4" +Bernard & Bianca +Beverly Hills Cop +Beverly Hills Cop II +Beverly Hills Cop III +Black Hawk Down +Blow +Blues Brothers +Blues Brothers 2000 +Bowfingers grosse Nummer +Braveheart +Brust oder Keule +Caddyshack +Chasing Amy +Cheech & Chong's The Corsican Brothers +Cheech & Chong im Dauerstress +Cheech & Chong Jetzt raucht überhaupt nichts mehr +Cheech & Chong Viel Rauch um Nichts +Cheech & Chongs Heisse Träume +Cocktail für eine Leiche +ConAir +Daredevil +Dark City +Das Appartment +Das Fenster zum Hof +Das fünfte Element +Das grosse Krabbeln +Das Jesus Video +Das kleine Arschloch +Das Krokodil und sein Nilpferd +Das Netz +Das Schweigen der Lämmer +Das Wunder von Bern +Der Boss +Der Club der toten Dichter +Der dritte Mann +Der Glückspilz +Der Hauch des Todes +Der kleine Horrorladen +Der Knochenjäger +Der Mann mit dem goldenen Colt +"Der Mann, der zuviel wußte" +Der Millionenfinger +Der Mondmann +Der Morgen stirbt nie +Der Name der Rose +Der Pate 1-3 +Der Puppenspieler +Der Querkopf +Der Schatzplanet +"Der Spion, der mich liebte" +Der talentierte Mr. Ripley +Der Totmacher +Der Wixxer +Diamantenfieber +"Dick und Doof erben eine Insel, Atoll K" +Didi - der Doppelgänger +Didi - Der Experte +Didi - Der Schnüffler +Didi - und die Rache der Enterbten +Didi auf vollen Touren +Die Ärzte Unplugged Rock'n' Roll Realschule +Die Einsteiger +"Die Geister, die ich rief" +Die Glücksjäger +Die Glücksritter +Die große Schlacht des Don Camillo +Die Harald Schmidt Show - Best Of Vol. 2 +Die Harald Schmidt Show - Best of +Die Jury +Die Liga der aussergewöhnlichen Gentlemen +Die Monster AG +Die Muppets erobern Manhattan +Die Muppets Weihnachtsgeschichte +Die nackte Kanone +Die nackte Kanone 2 1/2 +Die nackte Kanone 33 1/3 +Die neun Pforten +Die rechte und die linke Hand des Teufels +Die Truman Show +Die unglaubliche Reise in einem verrückten Flugzeug +Die unglaubliche Reise in einem verrückten Raumschiff +Die Unglaublichen +Die Verurteilten +Die Welt ist nicht genug +Die Wiege der Sonne +Diese Zwei sind nicht zu fassen +Dogma +Don Camillo und Peppone +Don Camillos Rückkehr +Dr. Dolittle 1+2 +Dragonheart +Drei Amigos +Ein Fisch namens Wanda +Ein Fisch namens Wanda +Ein irrer Typ +Ein Ticket für Zwei +Eine Leiche zum Dessert +"Eine schrecklich nette Familie +Dritte Staffel" +"Eine schrecklich nette Familie +Erste Staffel" +"Eins, Zwei, Drei" +Eiskalte Engel +El Dorado +Elektra +Es war einmal in Amerika +Evolution +Ey Mann - Wo is' mein Auto? +Faceoff - Im Körper des Feindes +Fahrenheit 9/11 +Falsches Spiel mit Roger Rabbit +Feuerball +Findet Nemo +Flammendes Inferno +Fletcher's Visionen +Fluch der Karibik +Forrest Gump +Four Rooms +Foxy Brown +Freddy vs. Jason +Freeze - Alptraum Nachtwache +Frenzy +Frequency +Fröhliche Ostern +From Dusk Till Dawn +From Dusk Till Dawn 2 +"From Dusk Till Dawn 3 +The Hangman's Daughter" +From Hell +Galaxy Quest +Gangs of New York +Gegen jede Regel +Geld oder Leber +Genosse Don Camillo +Ghost Ship +Ghostbusters 2 +Girls United +Glauben ist Alles! +God's Army +God's Army 3 +GoldenEye +Goldfinger +Good Bye Lenin! +Good Morning Vietnam +Good Will Hunting +Gottes Werk und Teufels Beitrag +Grosse Erwartungen +Hannibal +Harald Schmidt Best of Vol. 1+2 + Golden Goals +Helden aus der zweiten Reihe +Hercules +Hero +Hochwürden Don Camillo +Höllentour - Die Tour der Helden +Hot Shots 1 + 2 +Hulk +"I, Robot" +Ice Age +Im Angesicht des Todes +Im Geheimdienst Ihrer Majestät +Immer Ärger mit Bernie +In 80 Tagen um die Welt Teil 1 +In 80 Tagen um die Welt Teil 2 +In tödlicher Mission +Independence Day +Irma La Douce +Jabberwocky +Jackie Brown +Jagd auf einen Unsichtbaren +Jagd auf Roter Oktober +James Bond jagt Dr. No +JFK John F. Kennedy - Tatort Dallas +Johnny English +Jumanji +Kentucky Fried Movie +Kill Bill Vol. 1 +Kill Bill Vol. 2 +"Knight Moves +Ein mörderisches Spiel" +König der Fischer +La Boum +La Boum 2 +Leben und Sterben lassen +Liebesgrüße aus Moskau +Lizenz zum Töten +Lola rennt +Lost in Space +Lost In Translation +Louis und seine außerirdischen Kohlköpfe +Luther +Mallrats +Man lebt nur zweimal +Marillion Christmas in the Chapel +Marillion Live From Loreley +Marillion shot in the dark +Marnie +Master & Commander +Maverick +"MexiCollection +El Mariachi +Desperado +Irgendwann in Mexico" +MIB +MIB 2 +Missing +"Montys enzyklopythonia (Das Leben des Brian, Die Ritter der Kokosnuss, Der Sinn des Lebens)" +Moonraker - Streng Geheim +Mörderischer Vorsprung +Mr. Bean 1 +Mr. Bean 2 +Mr. Bean 3 +Muppets aus dem All +Muppets Die Schatzinsel +Mystic River +Nick Knatterton Teil 1 +Nick Knatterton Teil 2 +Nightmare Before Christmas +Nightwish - end of innocence +Nur 48 Stunden +"O Brother, Where Art Thou?" +Ocean's Eleven +Ocean's Twelve +Octopussy +"Onkel Paul, die große Pflaume" +Open Range +Oscar +Panic Room +Perdita Durango +Peter Gabriel Growing Up Live +Peter Gabriel Secret World Live +Platoon +Pulp Fiction +Rat Race Der nackte Wahnsinn +Reservoir Dogs +Resident Evil +Richy Guitar +Romeo Must Die +Roter Drache +Sag niemals nie +Scary Movie +Scary Movie 2 +Scharfe Kurven für Madame +Schiffsmeldungen +Schlappe Bullen beissen nicht +Scooby-Doo +Scream +Scream 2 +Scrubs: Die Anfänger - Die komplette erste Staffel (4 DVDs) +Scrubs: Die Anfänger - Die komplette zweite Staffel (4 DVDs) +Scrubs: Die Anfänger - Die komplette dritte Staffel (4 DVDs) +Shaft +Shakespeare in Love +Shang-High Noon +Shrek +Shrek 2 +Sideways +Sin City +Sin Eater +Sleepers +Sleepy Hollow +Small Soldiers +snatch Schweine und Diamanten +Solo für Zwei +Sonnenallee +South Park Der Film +Space Cowboys +Spaceballs +Speed Teil 1 + Teil 2 +Sphere +Spider-Man 2 +Spiel mir das Lied vom Tod +Stakeout II +Star Trek 1 +Star Trek 10 Nemesis +Star Trek 2 Der Zorn des Khan +Star Trek 3 Auf der Such nach Mr. Spock +Star Trek 4 Zurück in die Gegenwart +Star Trek 5 Am Rande des Universums +Star Trek 6 Das unentdeckte Land +Star Trek 7 Treffen der Generationen +Star Trek 8 Der erste Kontakt +Star Trek 9 Der Aufstand +Star Wars - Bonusmaterial +Star Wars - Clone Wars Vol. 1 +Star Wars - Clone Wars Vol. 2 +Star Wars - Episode 2 +Star Wars - Episode 3 - Die Rache der Sith +Star Wars - Episode IV - Eine neue Hoffnung +Star Wars - Episode V - Das Imperium schlägt zurück +Star Wars - Episode VI - Die Rückkehr der Jedi-Ritter +Stigmata +Stirb an einem anderen Tag +Stirb langsam 1+2 +Sumo Bruno +Tanz der Teufel +Tanz der Vampire +Terminator 2 Tag der Abrechnung +Terminator 3 Rebellion der Maschinen +The Abyss +The Art of War +The Big Lebowski +The Core +The Crow +The Day After Tomorrow +The Fog Nebel des Grauens +The Game +The Green Mile +The Rock +The Scorpion King +The Time Machine +Three Kings +Tiger & Dragon +Tomb Raider +Tomb Raider Die Wiege des Lebens +Topas +Toy Story +Toy Story 2 +Traffic Macht des Kartells +Troja +Tron +Twister +U-Turn +UHF +Und dann kam Polly +under suspicion Mörderisches Spiel +Underworld +Van Helsing +Verlockende Falle +Verrückt nach Mary +Verrückt nach mehr Mary +Vertigo +Vier Fäuste für ein Halleluja +Vier Fäuste gegen Rio +Volcano +Volker Pispers Live +Wayne's World +Wayne's World 2 +Wilde Kreaturen +X-Men +X-Men 2 +Zoolander +Zurück in die Zukunft Trilogie Boxset +Zwei Asse trumpfen auf +Zwei außer Rand und Band +Zwei bärenstarke Typen +Zwei Himmelhunde auf dem Weg zur Hölle +Zwei hinreissend verdorbene Schurken +Zwei Nasen tanken Super +Zwei sind nicht zu bremsen diff --git a/java-ee/Jenkinsfile b/java-ee/Jenkinsfile new file mode 100644 index 0000000..2f64305 --- /dev/null +++ b/java-ee/Jenkinsfile @@ -0,0 +1,15 @@ +node { + stage "Checkout" + checkout scm + stage 'Prepare Gradle build' + sh "chmod +x gradlew" + stage 'Stage Build' + sh "./gradlew --no-daemon clean build -x findbugsMain -x findbugsTest -x test" + stage 'Archive Artifacts' + archiveArtifacts allowEmptyArchive: true, artifacts: '**/build/reports/*/*.xml', defaultExcludes: false, onlyIfSuccessful: true + archiveArtifacts allowEmptyArchive: true, artifacts: '**/reports/*/*.html' + junit allowEmptyResults: true, testResults: '**/build/test-results/*.xml' + //step([$class: 'CheckStylePublisher', pattern: '**/build/reports/checkstyle/*.xml']) + //step([$class: 'FindBugsPublisher', pattern: '**/build/reports/findbugs/*.xml']) +} + diff --git a/java-ee/KontorApp/build.gradle b/java-ee/KontorApp/build.gradle new file mode 100644 index 0000000..437481b --- /dev/null +++ b/java-ee/KontorApp/build.gradle @@ -0,0 +1,19 @@ +apply plugin: 'java' +apply plugin: 'application' + +dependencies { + compile 'org.hibernate:hibernate-core:4.3.8.Final' + compile 'org.hibernate:hibernate-entitymanager:4.3.8.Final' + compile 'org.hsqldb:hsqldb:2.3.0' + compile 'ch.qos.logback:logback-core:1.1.2' + compile 'ch.qos.logback:logback-classic:1.1.2' + testCompile group: 'junit', name: 'junit', version: '4.11' +} + +mainClassName = 'com.ibtp.kontor.KontorApp' + +jar { + manifest { + attributes 'Implementation-Title': 'Kontor Application', 'Implementation-Version': version, 'Main-Class': mainClassName + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/KontorApp.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/KontorApp.java new file mode 100644 index 0000000..b87a849 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/KontorApp.java @@ -0,0 +1,23 @@ +package com.ibtp.kontor; + +/** + * Created by TPEETZ on 10.02.2015. + */ +public class KontorApp { + + private KontorGUI mainframe; + + public KontorApp() { + mainframe = new KontorGUI(this); + + mainframe.setVisible(true); + } + + public void exitApplication() { + System.exit(0); + } + + public static void main(String[] args) { + new KontorApp(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/KontorGUI.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/KontorGUI.java new file mode 100644 index 0000000..40a4b2d --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/KontorGUI.java @@ -0,0 +1,69 @@ +package com.ibtp.kontor; + + +import com.ibtp.kontor.comics.view.ComicsMenu; +import com.ibtp.kontor.library.view.LibraryMenu; +import com.ibtp.kontor.tradingcards.view.TradingCardsMenu; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +/** + * Created by TPEETZ on 11.02.2015. + */ +public class KontorGUI extends javax.swing.JFrame { + + KontorApp application; + JMenuBar menuBar; + JMenu menuFile; + JMenuItem menuFileExit; + + JMenuItem menuFileStart = new JMenuItem(); + + JMenu menuHelp; + JMenuItem menuHelpAbout; + + public KontorGUI(KontorApp kontorApp) { + application = kontorApp; + initComponents(); + } + + private void initComponents() { + setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + GroupLayout layout = new GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(GroupLayout.Alignment.LEADING).addGap(0, 400, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(GroupLayout.Alignment.LEADING).addGap(0, 300, Short.MAX_VALUE) + ); + pack(); + setTitle("Kontor Application"); + createMainMenu(); + //createToolBar(); + } + + private void createMainMenu() { + menuBar = new JMenuBar(); + menuFile = new JMenu("File"); + menuFileExit = new JMenuItem("Exit"); + menuHelp = new JMenu("Help"); + menuHelpAbout = new JMenuItem("About"); + setJMenuBar(menuBar); + menuBar.add(menuFile); + menuFile.add(menuFileExit); + menuBar.add(new ComicsMenu()); + menuBar.add(new LibraryMenu()); + menuBar.add(new TradingCardsMenu()); + menuBar.add(menuHelp); + menuHelp.add(menuHelpAbout); + menuFileExit.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + application.exitApplication(); + } + }); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ArtistDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ArtistDao.java new file mode 100644 index 0000000..00c4b01 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ArtistDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ArtistEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 16.01.2015. + */ +interface ArtistDao { + + public ArtistEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public Collection findAll(); + + public ArtistEntity addArtist(String name); + + public ArtistEntity store(ArtistEntity entity); + + public void delete(ArtistEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ArtistImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ArtistImpl.java new file mode 100644 index 0000000..1f5c604 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ArtistImpl.java @@ -0,0 +1,67 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ArtistEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 16.01.2015. + */ +public class ArtistImpl extends BaseImpl implements ArtistDao { + + public ArtistImpl() {} + + @Override + public ArtistEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Artist.findById"); + query.setParameter("id", id); + return (ArtistEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Artist.findAll"); + return query.getResultList(); + + } + + @Override + public Collection findByName(String name) { + Query query = getEntityManager().createNamedQuery("Artist.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public ArtistEntity addArtist(String name) { + ArtistEntity artist = new ArtistEntity(name); + artist = store(artist); + return artist; + } + + @Override + public ArtistEntity store(ArtistEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(ArtistEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ComicDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ComicDao.java new file mode 100644 index 0000000..c77fb89 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ComicDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ComicEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by thomas on 17.01.15. + */ +interface ComicDao { + + public ComicEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByTitle(String title); + + public Collection findAll(); + + public ComicEntity addComic(String title); + + public ComicEntity store(ComicEntity entity); + + public void delete(ComicEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ComicImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ComicImpl.java new file mode 100644 index 0000000..ecbad88 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ComicImpl.java @@ -0,0 +1,65 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ComicEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class ComicImpl extends BaseImpl implements ComicDao { + + @Override + public ComicEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Comic.findById"); + query.setParameter("id", id); + return (ComicEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByTitle(String title) { + Query query = getEntityManager().createNamedQuery("Comic.findByTitle"); + query.setParameter("title", title); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Comic.findAll"); + return query.getResultList(); + } + + @Override + public ComicEntity addComic(String title) { + ComicEntity comicEntity = new ComicEntity(); + comicEntity.setTitle(title); + store(comicEntity); + return comicEntity; + } + + @Override + public ComicEntity store(ComicEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(ComicEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/IssueDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/IssueDao.java new file mode 100644 index 0000000..49a5ab2 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/IssueDao.java @@ -0,0 +1,23 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.IssueEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +interface IssueDao { + public IssueEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByNumber(String number); + + public Collection findAll(); + + public IssueEntity store(IssueEntity entity); + + public void delete(IssueEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/IssueImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/IssueImpl.java new file mode 100644 index 0000000..8f70845 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/IssueImpl.java @@ -0,0 +1,57 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.IssueEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class IssueImpl extends BaseImpl implements IssueDao { + + @Override + public IssueEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Issue.findById"); + query.setParameter("id", id); + return (IssueEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByNumber(String number) { + Query query = getEntityManager().createNamedQuery("Issue.findByNumber"); + query.setParameter("number", number); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Issue.findAll"); + return query.getResultList(); + } + + @Override + public IssueEntity store(IssueEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(IssueEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/PublisherDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/PublisherDao.java new file mode 100644 index 0000000..0722522 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/PublisherDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.PublisherEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by thomas on 17.01.15. + */ +interface PublisherDao { + + public PublisherEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public Collection findAll(); + + public PublisherEntity addPublisher(String name); + + public PublisherEntity store(PublisherEntity entity); + + public void delete(PublisherEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/PublisherImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/PublisherImpl.java new file mode 100644 index 0000000..d911c4d --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/PublisherImpl.java @@ -0,0 +1,62 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.PublisherEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 20.01.2015. + */ +public class PublisherImpl extends BaseImpl implements PublisherDao { + + @Override + public PublisherEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + Query query = getEntityManager().createNamedQuery("Publisher.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Publisher.findAll"); + return query.getResultList(); + } + + @Override + public PublisherEntity addPublisher(String name) { + PublisherEntity publisher = new PublisherEntity(name); + store(publisher); + return publisher; + } + + @Override + public PublisherEntity store(PublisherEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(PublisherEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/StoryArcDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/StoryArcDao.java new file mode 100644 index 0000000..40761fe --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/StoryArcDao.java @@ -0,0 +1,24 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.StoryArcEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +interface StoryArcDao { + + public StoryArcEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByTitle(String title); + + public Collection findAll(); + + public StoryArcEntity store(StoryArcEntity entity); + + public void delete(StoryArcEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/StoryArcImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/StoryArcImpl.java new file mode 100644 index 0000000..3a39271 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/StoryArcImpl.java @@ -0,0 +1,55 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.StoryArcEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class StoryArcImpl extends BaseImpl implements StoryArcDao { + + @Override + public StoryArcEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByTitle(String title) { + Query query = getEntityManager().createNamedQuery("StoryArc.findByTitle"); + query.setParameter("title", title); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("StoryArc.findAll"); + return query.getResultList(); + } + + @Override + public StoryArcEntity store(StoryArcEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(StoryArcEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/VolumeDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/VolumeDao.java new file mode 100644 index 0000000..26759bd --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/VolumeDao.java @@ -0,0 +1,24 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.VolumeEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +interface VolumeDao { + + public VolumeEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByTitle(String title); + + public Collection findAll(); + + public VolumeEntity store(VolumeEntity entity); + + public void delete(VolumeEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/VolumeImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/VolumeImpl.java new file mode 100644 index 0000000..bb20a4a --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/VolumeImpl.java @@ -0,0 +1,57 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.VolumeEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class VolumeImpl extends BaseImpl implements VolumeDao { + + @Override + public VolumeEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Volume.findById"); + query.setParameter("id", id); + return (VolumeEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByTitle(String title) { + Query query = getEntityManager().createNamedQuery("Volume.findByTitle"); + query.setParameter("title", title); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Volume.findAll"); + return query.getResultList(); + } + + @Override + public VolumeEntity store(VolumeEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(VolumeEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/ArtistEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/ArtistEntity.java new file mode 100644 index 0000000..50ede83 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/ArtistEntity.java @@ -0,0 +1,72 @@ +package com.ibtp.kontor.comics.entity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by TPEETZ on 16.01.2015. + */ +@NamedQueries({ + @NamedQuery(name="Artist.findAll", query="SELECT a from ArtistEntity as a"), + @NamedQuery(name="Artist.findByName", query="SELECT a from ArtistEntity as a WHERE a.name = :name") +}) + +@Entity +@Table(name="ARTIST") +public class ArtistEntity { + + private Long id; + + private String name; + + private Collection writtenIssues = new ArrayList(); + + private Collection inkedIssues = new ArrayList(); + + private Collection penciledIssues = new ArrayList(); + + public ArtistEntity(String name) { + setName(name); + } + + public ArtistEntity() {} + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + String getName() { return name; } + + void setName(String name) { this.name = name; } + + public void setWrittenIssues(Collection writtenIssues) { this.writtenIssues = writtenIssues; } + + @OneToMany(mappedBy="writer", cascade=CascadeType.REMOVE) + public Collection getWrittenIssues() { + return writtenIssues; + } + + public void setInkedIssues(Collection inkedIssues) { this.inkedIssues = inkedIssues; } + + @OneToMany(mappedBy="inker", cascade=CascadeType.REMOVE) + public Collection getInkedIssues() { + return inkedIssues; + } + + public void setPenciledIssues(Collection penciledIssues) { this.penciledIssues = penciledIssues; } + + @OneToMany(mappedBy="penciler", cascade=CascadeType.REMOVE) + public Collection getPenciledIssues() { + return penciledIssues; + } + + @Override + public String toString() { + return "Artist[" + "id=" + getId() + ",name=" + getName() + "]"; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/ComicEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/ComicEntity.java new file mode 100644 index 0000000..2d21c10 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/ComicEntity.java @@ -0,0 +1,81 @@ +package com.ibtp.kontor.comics.entity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by thomas on 17.01.15. + */ +@NamedQueries({ + @NamedQuery(name="Comic.findAll", query="SELECT c from ComicEntity as c"), + @NamedQuery(name="Comic.findByTitle", query="SELECT c from ComicEntity as c WHERE c.title = :title") +}) +@Entity +@Table(name = "COMIC") +public class ComicEntity { + + private Long id; + + private String title; + + private Boolean completed; + + private Boolean currentOrder; + + private Collection issues = new ArrayList(); + + private Collection storyArc = new ArrayList(); + + private Collection volumes = new ArrayList(); + + private PublisherEntity publisher; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + @Column + public Boolean getCompleted() { return completed; } + + public Boolean isCompleted() { return completed; } + + public void setCompleted(Boolean completed) { this.completed = completed; } + + @Column + public Boolean getCurrentOrder() { return currentOrder; } + + public Boolean isCurrentOrder() { return currentOrder; } + + public void setCurrentOrder(Boolean currentOrder) { this.currentOrder = currentOrder; } + + public void setIssues(Collection issues) { this.issues = issues; } + + @OneToMany(mappedBy="comic", cascade=CascadeType.REMOVE) + public Collection getIssues() { return issues; } + + public void setStoryArc(Collection storyArc) { this.storyArc = storyArc; } + + @OneToMany(mappedBy="comic", cascade=CascadeType.REMOVE) + public Collection getStoryArc() { return storyArc; } + + public void setVolumes(Collection volumes) { this.volumes = volumes; } + + @OneToMany(mappedBy="comic", cascade=CascadeType.REMOVE) + public Collection getVolumes() { return volumes; } + + @ManyToOne + public PublisherEntity getPublisher() { return publisher; } + + public void setPublisher(PublisherEntity publisher) { + this.publisher = publisher; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/IssueEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/IssueEntity.java new file mode 100644 index 0000000..93a7662 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/IssueEntity.java @@ -0,0 +1,75 @@ +package com.ibtp.kontor.comics.entity; + +import javax.persistence.*; + +/** + * Created by thomas on 18.01.15. + */ +@NamedQueries({ + @NamedQuery(name="Issue.findAll", query="SELECT i from IssueEntity as i"), + @NamedQuery(name="Issue.findByNumber", query="SELECT i from IssueEntity as i WHERE i.number = :number") +}) + +@Entity +@Table(name = "ISSUE") +public class IssueEntity { + + private Long id; + + private String number; + + private Boolean completed; + + private ComicEntity comic; + + private ArtistEntity writer; + + private ArtistEntity inker; + + private ArtistEntity penciler; + + private StoryArcEntity storyArc; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getNumber() { return number; } + + public void setNumber(String number) { this.number = number; } + + @Column + public Boolean getCompleted() { return completed; } + public Boolean isCompleted() { return completed; } + + public void setCompleted(Boolean completed) { this.completed = completed; } + + public void setComic(ComicEntity comic) { this.comic = comic; } + + @ManyToOne + public ComicEntity getComic() { return comic; } + + public void setWriter(ArtistEntity writer) { this.writer = writer; } + + @ManyToOne + public ArtistEntity getWriter() { return writer; } + + public void setInker(ArtistEntity inker) { this.inker = inker; } + + @ManyToOne + public ArtistEntity getInker() { return inker; } + + public void setPenciler(ArtistEntity penciler) { this.penciler = penciler; } + + @ManyToOne + public ArtistEntity getPenciler() { return penciler; } + + public void setStoryArc(StoryArcEntity storyArc) { this.storyArc = storyArc; } + + @ManyToOne + public StoryArcEntity getStoryArc() { return storyArc; } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/PublisherEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/PublisherEntity.java new file mode 100644 index 0000000..9aec40e --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/PublisherEntity.java @@ -0,0 +1,47 @@ +package com.ibtp.kontor.comics.entity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by thomas on 17.01.15. + */ +@NamedQueries({ + @NamedQuery(name="Publisher.findAll", query="SELECT p from PublisherEntity as p"), + @NamedQuery(name="Publisher.findByName", query="SELECT p from PublisherEntity as p WHERE p.name = :name") +}) + +@Entity +@Table(name = "PUBLISHER") +public class PublisherEntity { + + private Long id; + + private String name; + + private Collection comic = new ArrayList(); + + public PublisherEntity() {} + + public PublisherEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy= GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { return name; } + + void setName(String name) { this.name = name; } + + public void setComic(Collection comic) { this.comic = comic; } + + @OneToMany(mappedBy="publisher", cascade=CascadeType.REMOVE) + public Collection getComic() { return comic; } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/StoryArcEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/StoryArcEntity.java new file mode 100644 index 0000000..e3ea22b --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/StoryArcEntity.java @@ -0,0 +1,48 @@ +package com.ibtp.kontor.comics.entity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by thomas on 17.01.15. + */ +@NamedQueries({ + @NamedQuery(name="StoryArc.findAll", query="SELECT s from StoryArcEntity as s"), + @NamedQuery(name="StoryArc.findByTitle", query="SELECT s from StoryArcEntity as s WHERE s.title = :title") +}) + +@Entity +@Table(name = "STORYARC") +public class StoryArcEntity { + + private Long id; + + private String title; + + private Collection issues = new ArrayList(); + + private ComicEntity comic; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + public void setIssues(Collection issues) { this.issues = issues; } + + @OneToMany(mappedBy="storyArc", cascade=CascadeType.REMOVE) + public Collection getIssues() { return issues; } + + public void setComic(ComicEntity comic) { this.comic = comic; } + + @ManyToOne + public ComicEntity getComic() { return comic; } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/VolumeEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/VolumeEntity.java new file mode 100644 index 0000000..6f896ad --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/VolumeEntity.java @@ -0,0 +1,40 @@ +package com.ibtp.kontor.comics.entity; + + +import javax.persistence.*; + +/** + * Created by TPEETZ on 19.01.2015. + */ +@NamedQueries({ + @NamedQuery(name="Volume.findAll", query="SELECT v from VolumeEntity as v"), + @NamedQuery(name="Volume.findByTitle", query="SELECT v from VolumeEntity as v WHERE v.title = :title") +}) + +@Entity +@Table(name = "VOLUME") +public class VolumeEntity { + + private Long id; + + private String title; + + private ComicEntity comic; + + @Id + @GeneratedValue(strategy= GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + @ManyToOne + public ComicEntity getComic() { return comic; } + + public void setComic(ComicEntity comic) { this.comic = comic; } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/view/ComicsMenu.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/view/ComicsMenu.java new file mode 100644 index 0000000..5201a11 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/view/ComicsMenu.java @@ -0,0 +1,13 @@ +package com.ibtp.kontor.comics.view; + +import javax.swing.*; + +/** + * Created by tpeetz on 12.02.2015. + */ +public class ComicsMenu extends JMenu { + + public ComicsMenu() { + super("Comics"); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/BaseImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/BaseImpl.java new file mode 100644 index 0000000..fa22722 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/BaseImpl.java @@ -0,0 +1,21 @@ +package com.ibtp.kontor.dal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.EntityManager; + +/** + * Created by TPEETZ on 16.01.2015. + */ +public class BaseImpl { + + protected BaseImpl() { + Logger logger = LoggerFactory.getLogger(this.getClass().getName()); + logger.info("BaseImpl started"); + } + + protected EntityManager getEntityManager() { + return DatabaseManager.getDatabase().getEntityManager(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/Database.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/Database.java new file mode 100644 index 0000000..67dbda9 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/Database.java @@ -0,0 +1,11 @@ +package com.ibtp.kontor.dal; + +import javax.persistence.EntityManager; + +/** + * Created by TPEETZ on 21.01.2015. + */ +public interface Database { + + public EntityManager getEntityManager(); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/DatabaseManager.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/DatabaseManager.java new file mode 100644 index 0000000..0ccc25d --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/DatabaseManager.java @@ -0,0 +1,23 @@ +package com.ibtp.kontor.dal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Created by TPEETZ on 22.01.2015. + */ +public class DatabaseManager { + + private static Database database; + private static Logger logger = LoggerFactory.getLogger(DatabaseManager.class.getName()); + + public static Database getDatabase() { + logger.info("return " + database.toString()); + return database; + } + + public static void setDatabase(Database database) { + logger.info("set " + database.toString()); + DatabaseManager.database = database; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/LocalDatabase.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/LocalDatabase.java new file mode 100644 index 0000000..37cc68b --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/LocalDatabase.java @@ -0,0 +1,103 @@ +package com.ibtp.kontor.dal; + +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hsqldb.Server; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import javax.persistence.spi.PersistenceProvider; +import javax.persistence.spi.PersistenceProviderResolver; +import javax.persistence.spi.PersistenceProviderResolverHolder; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +public class LocalDatabase implements Database { + + private static Server server; + private static EntityManagerFactory factory; + private static EntityManager em; + private static Logger logger = LoggerFactory.getLogger(LocalDatabase.class.getName()); + + private LocalDatabase() { + logger.info("LocalDatabase started"); + } + + private static void assureDatabaseRunning() { + if (LocalDatabase.server == null) { + LocalDatabase.startDatabase(); + } + } + + private static void startDatabase() { + Logger logger = LoggerFactory.getLogger(LocalDatabase.class.getName()); + logger.info("startDatabase as kontor in hsqldb_databases/kontor"); + LocalDatabase.server = new Server(); + LocalDatabase.server.setAddress("localhost"); + LocalDatabase.server.setDatabaseName(0, "kontor"); + LocalDatabase.server.setDatabasePath(0, "file:hsqldb_databases/kontor"); + LocalDatabase.server.setPort(2345); + LocalDatabase.server.setTrace(true); + LocalDatabase.server.setLogWriter(new PrintWriter(System.out)); + LocalDatabase.server.start(); + } + + private static void stopDatabase() { + server.shutdown(); + } + private static EntityManagerFactory getFactory() { + if (LocalDatabase.factory == null) { + LocalDatabase.assureDatabaseRunning(); + PersistenceProviderResolverHolder.setPersistenceProviderResolver(new PersistenceProviderResolver() { + private final List providers_ = Arrays.asList((PersistenceProvider) new HibernatePersistenceProvider()); + + @Override + public void clearCachedProviders() { + // Auto-generated method stub + } + + @Override + public List getPersistenceProviders() { + return providers_; + } + }); + LocalDatabase.factory = Persistence.createEntityManagerFactory("com.ibtp.kontor"); + logger.info("EntityManagerFactory(com.ibtp.kontor) created"); + } + return LocalDatabase.factory; + } + + private static EntityManager getSingleEntityManager() { + return LocalDatabase.em; + } + + private static void setSingleEntityManager(EntityManager manager) { + LocalDatabase.em = manager; + } + + @Override + public EntityManager getEntityManager() { + if (getSingleEntityManager() == null) { + setSingleEntityManager(getFactory().createEntityManager()); + logger.info("EntityManager created"); + } + return getSingleEntityManager(); + } + + @Override + public String toString() { + String serverMessage; + if (LocalDatabase.server == null) { + serverMessage = "server:null"; + } else { + serverMessage = LocalDatabase.server.toString(); + } + return LocalDatabase.class.getName() + " " + serverMessage; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/ArticleDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/ArticleDao.java new file mode 100644 index 0000000..f9cd144 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/ArticleDao.java @@ -0,0 +1,23 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.library.entity.ArticleEntity; + +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +interface ArticleDao { + + public ArticleEntity getById(Long id); + + public List findByIds(List ids); + + public List findAll(); + + public List findByTitle(String title); + + public ArticleEntity store(ArticleEntity entity); + + public void delete(ArticleEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/ArticleImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/ArticleImpl.java new file mode 100644 index 0000000..ada92d9 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/ArticleImpl.java @@ -0,0 +1,64 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.library.entity.ArticleEntity; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +public class ArticleImpl extends BaseImpl implements ArticleDao { + + @Override + public ArticleEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Article.findById"); + query.setParameter("id", id); + return (ArticleEntity)query.getSingleResult(); + } + + @Override + public List findByIds(List ids) { + return null; + } + + @Override + public List findAll() { + Query query = getEntityManager().createNamedQuery("Article.findAll"); + //noinspection unchecked + return query.getResultList(); + } + + @Override + public List findByTitle(String title) { + Query query = getEntityManager().createNamedQuery("Article.findByTitle"); + query.setParameter("title", title); + //noinspection unchecked + return query.getResultList(); + } + + public ArticleEntity addArticle(String title) { + ArticleEntity entity = new ArticleEntity(title); + store(entity); + return entity; + } + + @Override + public ArticleEntity store(ArticleEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(ArticleEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/AuthorDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/AuthorDao.java new file mode 100644 index 0000000..85f4c83 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/AuthorDao.java @@ -0,0 +1,25 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.library.entity.AuthorEntity; + +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +interface AuthorDao { + + public AuthorEntity getById(Long id); + + public List findByIds(List ids); + + public List findByName(String name); + + public List findAll(); + + public AuthorEntity addAuthor(String name); + + public AuthorEntity store(AuthorEntity entity); + + public void delete(AuthorEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/AuthorImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/AuthorImpl.java new file mode 100644 index 0000000..573d7d9 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/AuthorImpl.java @@ -0,0 +1,65 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.library.entity.AuthorEntity; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.List; + +/** + * Created by thomas on 23.01.15. + */ +public class AuthorImpl extends BaseImpl implements AuthorDao { + + public AuthorImpl() {} + + @Override + public AuthorEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Author.findById"); + query.setParameter("id", id); + return (AuthorEntity)query.getSingleResult(); + } + + @Override + public List findByIds(List ids) { + return null; + } + + @Override + public List findByName(String name) { + Query query = getEntityManager().createNamedQuery("Author.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public List findAll() { + Query query = getEntityManager().createNamedQuery("Author.findAll"); + return query.getResultList(); + } + + @Override + public AuthorEntity addAuthor(String name) { + AuthorEntity author = new AuthorEntity(name); + store(author); + return author; + } + + @Override + public AuthorEntity store(AuthorEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(AuthorEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/BookDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/BookDao.java new file mode 100644 index 0000000..d89a1b7 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/BookDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.library.entity.BookEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +interface BookDao { + + public BookEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public BookEntity store(BookEntity entity); + + public void delete(BookEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/BookImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/BookImpl.java new file mode 100644 index 0000000..63f0ab1 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/BookImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.library.entity.BookEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class BookImpl extends BaseImpl implements BookDao { + + @Override + public BookEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public BookEntity store(BookEntity entity) { + return null; + } + + @Override + public void delete(BookEntity entity) { + + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/FileDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/FileDao.java new file mode 100644 index 0000000..f123e69 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/FileDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.library.entity.FileEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +interface FileDao { + + public FileEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public FileEntity store(FileEntity entity); + + public void delete(FileEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/FileImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/FileImpl.java new file mode 100644 index 0000000..1af2900 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/FileImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.library.entity.FileEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class FileImpl extends BaseImpl implements FileDao { + + @Override + public FileEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public FileEntity store(FileEntity entity) { + return null; + } + + @Override + public void delete(FileEntity entity) { + + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/TitleDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/TitleDao.java new file mode 100644 index 0000000..81da92b --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/TitleDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.library.entity.TitleEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +interface TitleDao { + + public TitleEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public TitleEntity store(TitleEntity entity); + + public void delete(TitleEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/TitleImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/TitleImpl.java new file mode 100644 index 0000000..410c2c5 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/TitleImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.library.entity.TitleEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class TitleImpl extends BaseImpl implements TitleDao { + + @Override + public TitleEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public TitleEntity store(TitleEntity entity) { + return null; + } + + @Override + public void delete(TitleEntity entity) { + + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/ArticleEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/ArticleEntity.java new file mode 100644 index 0000000..d61eda5 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/ArticleEntity.java @@ -0,0 +1,56 @@ +package com.ibtp.kontor.library.entity; + +import javax.persistence.*; + +/** + * Created by TPEETZ on 23.01.2015. + */ +@NamedQueries({ + @NamedQuery(name="Article.findAll", query="SELECT a from ArticleEntity as a"), + @NamedQuery(name="Article.findByTitle", query="SELECT a from ArticleEntity as a WHERE a.title = :title") +}) + +@Entity +@Table(name = "ARTICLE") +public class ArticleEntity { + + private Long id; + + private String title; + + private AuthorEntity author; + + public ArticleEntity() {} + + public ArticleEntity(String title) { + setTitle(title); + } + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @Column + public String getTitle() { + return title; + } + + void setTitle(String title) { + this.title = title; + } + + @ManyToOne + public AuthorEntity getAuthor() { + return author; + } + + public void setAuthor(AuthorEntity author) { + this.author = author; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/AuthorEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/AuthorEntity.java new file mode 100644 index 0000000..63a6f4b --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/AuthorEntity.java @@ -0,0 +1,62 @@ +package com.ibtp.kontor.library.entity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by TPEETZ on 23.01.2015. + */ +@NamedQueries({ + @NamedQuery(name="Author.findAll", query="SELECT a from AuthorEntity as a"), + @NamedQuery(name="Author.findById", query="SELECT a from AuthorEntity as a WHERE a.id = :id"), + @NamedQuery(name="Author.findByName", query="SELECT a from AuthorEntity as a WHERE a.name = :name") +}) +@Entity +@Table(name="AUTHOR") +public class AuthorEntity { + + private Long id; + + private String name; + + private Collection books = new ArrayList(); + + private Collection articles = new ArrayList(); + + public AuthorEntity() {} + + public AuthorEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy= GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { return name; } + + void setName(String name) { this.name = name; } + + @OneToMany(mappedBy="author", cascade=CascadeType.REMOVE) + public Collection getBooks() { + return books; + } + + public void setBooks(Collection books) { + this.books = books; + } + + @OneToMany(mappedBy="author", cascade=CascadeType.REMOVE) + public Collection getArticles() { + return articles; + } + + public void setArticles(Collection articles) { + this.articles = articles; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/BookEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/BookEntity.java new file mode 100644 index 0000000..b57705c --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/BookEntity.java @@ -0,0 +1,96 @@ +package com.ibtp.kontor.library.entity; + +import javax.persistence.*; + +/** + * Created by TPEETZ on 23.01.2015. + */ +@Entity +@Table(name = "BOOK") +public class BookEntity { + + private Long id; + + private String title; + + private AuthorEntity author; + + private String publisher; + + private String isbn; + + private Long page; + + private String edition; + + public BookEntity() {} + + public BookEntity(String title) { + setTitle(title); + } + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + public Long getId() { + return id; + } + + /* unused */ + public void setId(Long id) { + this.id = id; + } + + @Column + public String getTitle() { + return title; + } + + void setTitle(String title) { + this.title = title; + } + + @ManyToOne + public AuthorEntity getAuthor() { + return author; + } + + public void setAuthor(AuthorEntity author) { + this.author = author; + } + + @Column + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + @Column + public Long getPage() { + return page; + } + + public void setPage(Long page) { + this.page = page; + } + + @Column + public String getEdition() { + return edition; + } + + public void setEdition(String edition) { + this.edition = edition; + } + + @Column + public String getPublisher() { + return publisher; + } + + public void setPublisher(String publisher) { + this.publisher = publisher; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/FileEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/FileEntity.java new file mode 100644 index 0000000..05f4577 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/FileEntity.java @@ -0,0 +1,41 @@ +package com.ibtp.kontor.library.entity; + +import javax.persistence.*; + +/** + * Created by TPEETZ on 23.01.2015. + */ +@Entity +@Table(name = "FILE") +public class FileEntity { + + private Long id; + + private String title; + + public FileEntity() {} + + public FileEntity(String title) { + setTitle(title); + } + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + public Long getId() { + return id; + } + + /* unused */ + public void setId(Long id) { + this.id = id; + } + + @Column + public String getTitle() { + return title; + } + + void setTitle(String title) { + this.title = title; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/TitleEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/TitleEntity.java new file mode 100644 index 0000000..ba25eb9 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/TitleEntity.java @@ -0,0 +1,27 @@ +package com.ibtp.kontor.library.entity; + +import javax.persistence.*; + +/** + * Created by TPEETZ on 23.01.2015. + */ +@Entity +@Table(name = "TITLE") +public class TitleEntity { + + private Long id; + + private String title; + + @Id + @GeneratedValue(strategy= GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/view/LibraryMenu.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/view/LibraryMenu.java new file mode 100644 index 0000000..0b97e27 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/view/LibraryMenu.java @@ -0,0 +1,13 @@ +package com.ibtp.kontor.library.view; + +import javax.swing.*; + +/** + * Created by tpeetz on 12.02.2015. + */ +public class LibraryMenu extends JMenu { + + public LibraryMenu() { + super("Library"); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetDao.java new file mode 100644 index 0000000..29692d4 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.BaseSetEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface BaseSetDao { + + public BaseSetEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public BaseSetEntity store(BaseSetEntity entity); + + public void delete(BaseSetEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetImpl.java new file mode 100644 index 0000000..9e584a4 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetImpl.java @@ -0,0 +1,51 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.BaseSetEntity; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class BaseSetImpl extends BaseImpl implements BaseSetDao { + + @Override + public BaseSetEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("BaseSet.findById"); + query.setParameter("id", id); + return (BaseSetEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + Query query = getEntityManager().createNamedQuery("BaseSet.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public BaseSetEntity store(BaseSetEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(BaseSetEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertDao.java new file mode 100644 index 0000000..c68790f --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.InsertEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface InsertDao { + + public InsertEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public InsertEntity store(InsertEntity entity); + + public void delete(InsertEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertImpl.java new file mode 100644 index 0000000..598763c --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.InsertEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class InsertImpl extends BaseImpl implements InsertDao { + + @Override + public InsertEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public InsertEntity store(InsertEntity entity) { + return null; + } + + @Override + public void delete(InsertEntity entity) { + + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerDao.java new file mode 100644 index 0000000..451d18b --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerDao.java @@ -0,0 +1,29 @@ +package com.ibtp.kontor.tradingcards.dal; + + +import com.ibtp.kontor.tradingcards.entity.BaseSetEntity; +import com.ibtp.kontor.tradingcards.entity.ManufacturerEntity; + +import java.util.Collection; +import java.util.List; + +/** + * + * @author tpeetz + */ +interface ManufacturerDao { + + public ManufacturerEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public ManufacturerEntity assignBaseSet(ManufacturerEntity manufacturer, BaseSetEntity baseSet); + + public ManufacturerEntity addManufacturer(String name); + + public ManufacturerEntity store(ManufacturerEntity entity); + + public void delete(ManufacturerEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImpl.java new file mode 100644 index 0000000..58c2fd6 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImpl.java @@ -0,0 +1,65 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.BaseSetEntity; +import com.ibtp.kontor.tradingcards.entity.ManufacturerEntity; + +import java.util.List; +import javax.persistence.EntityManager; +import javax.persistence.Query; + +/** + * + * @author tpeetz + */ +public class ManufacturerImpl extends BaseImpl implements ManufacturerDao { + + @Override + public ManufacturerEntity getById(Long id) { + Query q = getEntityManager().createNamedQuery("Manufacturer.findById"); + q.setParameter("id", id); + return (ManufacturerEntity)q.getSingleResult(); + } + + @Override + public List findByIds(List ids) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @SuppressWarnings("unchecked") + @Override + public List findByName(String name) { + Query q = getEntityManager().createNamedQuery("Manufacturer.findByName"); + q.setParameter("name", name); + return q.getResultList(); + } + + @Override + public ManufacturerEntity assignBaseSet(ManufacturerEntity comic, BaseSetEntity baseSet) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public ManufacturerEntity addManufacturer(String name) { + ManufacturerEntity manufacturer = new ManufacturerEntity(name); + store(manufacturer); + return manufacturer; + } + + @Override + public ManufacturerEntity store(ManufacturerEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(ManufacturerEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetDao.java new file mode 100644 index 0000000..c4d36ba --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.ParallelSetEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface ParallelSetDao { + + public ParallelSetEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public ParallelSetEntity store(ParallelSetEntity entity); + + public void delete(ParallelSetEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImpl.java new file mode 100644 index 0000000..cb604a0 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.ParallelSetEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class ParallelSetImpl extends BaseImpl implements ParallelSetDao { + + @Override + public ParallelSetEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public ParallelSetEntity store(ParallelSetEntity entity) { + return null; + } + + @Override + public void delete(ParallelSetEntity entity) { + + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerDao.java new file mode 100644 index 0000000..11d045e --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerDao.java @@ -0,0 +1,24 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.PlayerEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface PlayerDao { + + public PlayerEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public PlayerEntity addPlayer(String name); + + public PlayerEntity store(PlayerEntity entity); + + public void delete(PlayerEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerImpl.java new file mode 100644 index 0000000..c3d5104 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerImpl.java @@ -0,0 +1,43 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.PlayerEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class PlayerImpl extends BaseImpl implements PlayerDao { + + @Override + public PlayerEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public PlayerEntity addPlayer(String name) { + return null; + } + + @Override + public PlayerEntity store(PlayerEntity entity) { + return null; + } + + @Override + public void delete(PlayerEntity entity) { + + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionDao.java new file mode 100644 index 0000000..9970491 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.PositionEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface PositionDao { + + public PositionEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public Collection findAll(); + + public PositionEntity addPosition(String name); + + public PositionEntity store(PositionEntity entity); + + public void delete(PositionEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionImpl.java new file mode 100644 index 0000000..8aad906 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionImpl.java @@ -0,0 +1,64 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.PositionEntity; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class PositionImpl extends BaseImpl implements PositionDao { + + @Override + public PositionEntity getById(Long id) { + Query q = getEntityManager().createNamedQuery("Position.findById"); + q.setParameter("id", id); + return (PositionEntity)q.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + Query q = getEntityManager().createNamedQuery("Position.findByName"); + q.setParameter("name", name); + return q.getResultList(); + } + + @Override + public Collection findAll() { + Query q = getEntityManager().createNamedQuery("Position.findAll"); + return q.getResultList(); + } + + @Override + public PositionEntity addPosition(String name) { + PositionEntity position = new PositionEntity(name); + store(position); + return position; + } + + @Override + public PositionEntity store(PositionEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(PositionEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardDao.java new file mode 100644 index 0000000..25fb244 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.SportCardEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface SportCardDao { + + public SportCardEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public SportCardEntity store(SportCardEntity entity); + + public void delete(SportCardEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardImpl.java new file mode 100644 index 0000000..5939d2c --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.SportCardEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class SportCardImpl extends BaseImpl implements SportCardDao { + + @Override + public SportCardEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public SportCardEntity store(SportCardEntity entity) { + return null; + } + + @Override + public void delete(SportCardEntity entity) { + + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportDao.java new file mode 100644 index 0000000..f5ba51e --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.SportEntity; + +import java.util.Collection; +import java.util.List; + +/** + * + * @author tpeetz + */ +interface SportDao { + public SportEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public Collection findAll(); + + public SportEntity addSport(String name); + + public SportEntity store(SportEntity entity); + + public void delete(SportEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportImpl.java new file mode 100644 index 0000000..223236d --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportImpl.java @@ -0,0 +1,64 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.SportEntity; + +import java.util.Collection; +import java.util.List; +import javax.persistence.EntityManager; +import javax.persistence.Query; + +/** + * + * @author tpeetz + */ +public class SportImpl extends BaseImpl implements SportDao { + + @Override + public SportEntity getById(Long id) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public List findByIds(List ids) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @SuppressWarnings("unchecked") + @Override + public List findByName(String name) { + Query query = getEntityManager().createNamedQuery("Sport.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Sport.findAll"); + return query.getResultList(); + } + + @Override + public SportEntity addSport(String name) { + SportEntity sport = new SportEntity(name); + store(sport); + return sport; + } + + @Override + public SportEntity store(SportEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(SportEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamDao.java new file mode 100644 index 0000000..c2f9eff --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.SportEntity; +import com.ibtp.kontor.tradingcards.entity.TeamEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +interface TeamDao { + public TeamEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public Collection findAll(); + + public TeamEntity addTeam(String name, SportEntity sport); + + public TeamEntity store(TeamEntity entity); + + public void delete(TeamEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamImpl.java new file mode 100644 index 0000000..7fb8244 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamImpl.java @@ -0,0 +1,66 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.SportEntity; +import com.ibtp.kontor.tradingcards.entity.TeamEntity; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +public class TeamImpl extends BaseImpl implements TeamDao { + + @Override + public TeamEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Team.findById"); + query.setParameter("id", id); + return (TeamEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + Query query = getEntityManager().createNamedQuery("Team.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Team.findAll"); + return query.getResultList(); + } + + @Override + public TeamEntity addTeam(String name, SportEntity sport) { + TeamEntity team = new TeamEntity(name); + team.setSport(sport); + store(team); + return team; + } + + @Override + public TeamEntity store(TeamEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(TeamEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/BaseSetEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/BaseSetEntity.java new file mode 100644 index 0000000..3111e98 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/BaseSetEntity.java @@ -0,0 +1,67 @@ +package com.ibtp.kontor.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="BASESET") +public class BaseSetEntity { + private Long id; + private String name; + private ManufacturerEntity manufacturer; + private Collection parallelSets = new ArrayList(); + private Collection inserts = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ManyToOne + public ManufacturerEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(ManufacturerEntity manufacturer) { + this.manufacturer = manufacturer; + } + + @OneToMany(mappedBy="baseSet", cascade=CascadeType.REMOVE) + public Collection getParallelSets() { + return parallelSets; + } + + public void setParallelSets(Collection parallelSets) { + this.parallelSets = parallelSets; + } + + @OneToMany(mappedBy="baseSet", cascade=CascadeType.REMOVE) + public Collection getInserts() { + return inserts; + } + + public void setInserts(Collection inserts) { + this.inserts = inserts; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/InsertEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/InsertEntity.java new file mode 100644 index 0000000..ce59037 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/InsertEntity.java @@ -0,0 +1,74 @@ +package com.ibtp.kontor.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="InsertSet.findAll", query="SELECT i from InsertEntity as i"), + @NamedQuery(name="InsertSet.findById", query="SELECT i from InsertEntity as i WHERE i.id = :id"), + @NamedQuery(name="InsertSet.findByName", query="SELECT i from InsertEntity as i WHERE i.name = :name") +}) + +@Entity +@Table(name="INSERTSET") +public class InsertEntity { + private Long id; + private String name; + private ManufacturerEntity manufacturer; + private BaseSetEntity baseSet; + private Collection sportCard = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ManyToOne + public ManufacturerEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(ManufacturerEntity manufacturer) { + this.manufacturer = manufacturer; + } + + @ManyToOne + public BaseSetEntity getBaseSet() { + return baseSet; + } + + public void setBaseSet(BaseSetEntity baseSet) { + this.baseSet = baseSet; + } + + @OneToMany(mappedBy="insert", cascade=CascadeType.REMOVE) + public Collection getSportCard() { + return sportCard; + } + + public void setSportCard(Collection sportCard) { + this.sportCard = sportCard; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/ManufacturerEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/ManufacturerEntity.java new file mode 100644 index 0000000..f83da2a --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/ManufacturerEntity.java @@ -0,0 +1,78 @@ +package com.ibtp.kontor.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Manufacturer.findAll", query="SELECT m from ManufacturerEntity as m"), + @NamedQuery(name="Manufacturer.findByName", query="SELECT m from ManufacturerEntity as m WHERE m.name = :name") +}) + +@Entity +@Table(name="MANUFACTURER") +public class ManufacturerEntity { + + private Long id; + private String name; + private Collection baseSets = new ArrayList(); + private Collection parallelSets = new ArrayList(); + private Collection inserts = new ArrayList(); + + public ManufacturerEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + void setName(String name) { + this.name = name; + } + + @OneToMany(mappedBy="manufacturer", cascade=CascadeType.REMOVE) + public Collection getBaseSets() { + return baseSets; + } + + public void setBaseSets(Collection baseSets) { + this.baseSets = baseSets; + } + + @OneToMany(mappedBy="manufacturer", cascade=CascadeType.REMOVE) + public Collection getParallelSets() { + return parallelSets; + } + + public void setParallelSets(Collection parallelSets) { + this.parallelSets = parallelSets; + } + + @OneToMany(mappedBy="manufacturer", cascade=CascadeType.REMOVE) + public Collection getInserts() { + return inserts; + } + + public void setInserts(Collection inserts) { + this.inserts = inserts; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/ParallelSetEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/ParallelSetEntity.java new file mode 100644 index 0000000..0ce0c69 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/ParallelSetEntity.java @@ -0,0 +1,53 @@ +package com.ibtp.kontor.tradingcards.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name="PARALLELSET") +public class ParallelSetEntity { + private Long id; + private String name; + private ManufacturerEntity manufacturer; + + private BaseSetEntity baseSet; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + @ManyToOne + public ManufacturerEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(ManufacturerEntity manufacturer) { + this.manufacturer = manufacturer; + } + + public void setName(String name) { + this.name = name; + } + + @ManyToOne + public BaseSetEntity getBaseSet() { + return baseSet; + } + + public void setBaseSet(BaseSetEntity baseSet) { + this.baseSet = baseSet; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/PlayerEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/PlayerEntity.java new file mode 100644 index 0000000..7c19d70 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/PlayerEntity.java @@ -0,0 +1,46 @@ +package com.ibtp.kontor.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="PLAYER") +public class PlayerEntity { + private Long id; + private TeamEntity team; + private Collection cards = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @ManyToOne + public TeamEntity getTeam() { + return team; + } + + public void setTeam(TeamEntity team) { + this.team = team; + } + + @OneToMany(mappedBy="player", cascade=CascadeType.REMOVE) + public Collection getCards() { + return cards; + } + + public void setCards(Collection cards) { + this.cards = cards; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/PositionEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/PositionEntity.java new file mode 100644 index 0000000..3e575e0 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/PositionEntity.java @@ -0,0 +1,56 @@ +package com.ibtp.kontor.tradingcards.entity; + +import javax.persistence.*; + +@NamedQueries({ + @NamedQuery(name="Position.findAll", query="SELECT m from PositionEntity as m"), + @NamedQuery(name="Position.findByName", query="SELECT m from PositionEntity as m WHERE m.name = :name") +}) + +@Entity +@Table(name="POSITION") +public class PositionEntity { + + private Long id; + private String name; + private String shortName; + private SportEntity sport; + + public PositionEntity() {} + + public PositionEntity(String name) { setName(name); } + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Column + public String getShortName() { + return shortName; + } + + public void setShortName(String shortName) { + this.shortName = shortName; + } + + @ManyToOne + public SportEntity getSport() { + return sport; + } + + public void setSport(SportEntity sport) { + this.sport = sport; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/SportCardEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/SportCardEntity.java new file mode 100644 index 0000000..f71f722 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/SportCardEntity.java @@ -0,0 +1,68 @@ +package com.ibtp.kontor.tradingcards.entity; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="SportCard.findAll", query="SELECT s from SportCardEntity as s") +}) + +@Entity +@Table(name = "SPORTCARD") +public class SportCardEntity { + private Long id; + private PlayerEntity player; + private BaseSetEntity baseSet; + private ParallelSetEntity parallelSet; + private InsertEntity insert; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @ManyToOne + public PlayerEntity getPlayer() { + return player; + } + + public void setPlayer(PlayerEntity player) { + this.player = player; + } + + @ManyToOne + public BaseSetEntity getBaseSet() { + return baseSet; + } + + public void setBaseSet(BaseSetEntity baseSet) { + this.baseSet = baseSet; + } + + @ManyToOne + public ParallelSetEntity getParallelSet() { + return parallelSet; + } + + public void setParallelSet(ParallelSetEntity parallelSet) { + this.parallelSet = parallelSet; + } + + @ManyToOne + public InsertEntity getInsert() { + return insert; + } + + public void setInsert(InsertEntity insert) { + this.insert = insert; + } + +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/SportEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/SportEntity.java new file mode 100644 index 0000000..8e2258e --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/SportEntity.java @@ -0,0 +1,75 @@ +package com.ibtp.kontor.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Sport.findAll", query="SELECT s from SportEntity as s"), + @NamedQuery(name="Sport.findByName", query="SELECT s from SportEntity as s WHERE s.name = :name") +}) + +@Entity +@Table(name="SPORT") +public class SportEntity { + + private Long id; + private String name; + private Collection teams = new ArrayList(); + private Collection positions = new ArrayList(); + + public SportEntity() {} + + public SportEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + String getName() { + return name; + } + + void setName(String name) { + this.name = name; + } + + @OneToMany(mappedBy="sport", cascade=CascadeType.REMOVE) + public Collection getTeams() { + return teams; + } + + public void setTeams(Collection teams) { + this.teams = teams; + } + + @OneToMany(mappedBy="sport", cascade=CascadeType.REMOVE) + public Collection getPositions() { + return positions; + } + + public void setPositions(Collection positions) { + this.positions = positions; + } + + @Override + public String toString() { + return "Sport[" + "id=" + getId() + ",name=" + getName() + "]"; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/TeamEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/TeamEntity.java new file mode 100644 index 0000000..8dbcb77 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/TeamEntity.java @@ -0,0 +1,48 @@ +package com.ibtp.kontor.tradingcards.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Team.findAll", query="SELECT t from TeamEntity as t"), + @NamedQuery(name="Team.findByName", query="SELECT t from TeamEntity as t WHERE t.name = :name") +}) + +@Entity +@Table(name="TEAM") +public class TeamEntity { + + private Long id; + private String name; + private SportEntity sport; + + public TeamEntity() {} + + public TeamEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { return name; } + + void setName(String name) { this.name = name; } + + @ManyToOne + public SportEntity getSport() { return sport; } + + public void setSport(SportEntity sport) { this.sport = sport; } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/view/TradingCardsMenu.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/view/TradingCardsMenu.java new file mode 100644 index 0000000..f124056 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/view/TradingCardsMenu.java @@ -0,0 +1,13 @@ +package com.ibtp.kontor.tradingcards.view; + +import javax.swing.*; + +/** + * Created by TPEETZ on 13.02.2015. + */ +public class TradingCardsMenu extends JMenu { + + public TradingCardsMenu() { + super("TradingCards"); + } +} diff --git a/java-ee/KontorApp/src/main/resources/META-INF/persistence.xml b/java-ee/KontorApp/src/main/resources/META-INF/persistence.xml new file mode 100644 index 0000000..35d671b --- /dev/null +++ b/java-ee/KontorApp/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,39 @@ + + + org.hibernate.jpa.HibernatePersistenceProvider + com.ibtp.kontor.comics.entity.ArtistEntity + com.ibtp.kontor.comics.entity.ComicEntity + com.ibtp.kontor.comics.entity.IssueEntity + com.ibtp.kontor.comics.entity.StoryArcEntity + com.ibtp.kontor.comics.entity.VolumeEntity + com.ibtp.kontor.comics.entity.PublisherEntity + com.ibtp.kontor.library.entity.AuthorEntity + com.ibtp.kontor.library.entity.ArticleEntity + com.ibtp.kontor.library.entity.BookEntity + com.ibtp.kontor.library.entity.FileEntity + com.ibtp.kontor.library.entity.TitleEntity + com.ibtp.kontor.tradingcards.entity.SportEntity + com.ibtp.kontor.tradingcards.entity.TeamEntity + com.ibtp.kontor.tradingcards.entity.PositionEntity + com.ibtp.kontor.tradingcards.entity.PlayerEntity + com.ibtp.kontor.tradingcards.entity.ManufacturerEntity + com.ibtp.kontor.tradingcards.entity.BaseSetEntity + com.ibtp.kontor.tradingcards.entity.InsertEntity + com.ibtp.kontor.tradingcards.entity.ParallelSetEntity + com.ibtp.kontor.tradingcards.entity.SportCardEntity + + + + + + + + + + + + + diff --git a/java-ee/KontorApp/src/main/resources/logback.xml b/java-ee/KontorApp/src/main/resources/logback.xml new file mode 100644 index 0000000..e8cff02 --- /dev/null +++ b/java-ee/KontorApp/src/main/resources/logback.xml @@ -0,0 +1,40 @@ + + + + + + %d{yyyy-MM-dd_HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + + + + + c:/kontor.log + + %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + c:/kontor.%i.log.zip + 1 + 10 + + + + 2MB + + + + + + + + + + + + + diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/CollectionTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/CollectionTest.java new file mode 100644 index 0000000..146c854 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/CollectionTest.java @@ -0,0 +1,41 @@ +package com.ibtp.kontor.comics; + +import com.ibtp.kontor.comics.dal.PublisherImpl; +import com.ibtp.kontor.comics.entity.PublisherEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.*; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class CollectionTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @After + public void cleanup() { + PublisherImpl publisherImpl = new PublisherImpl(); + Collection publisherEntities = publisherImpl.findAll(); + for (Iterator iterator = publisherEntities.iterator(); iterator.hasNext(); ) { + PublisherEntity next = iterator.next(); + publisherImpl.delete(next); + } + } + + @Test + public void testAddPublishers() { + String publisherName = "Bongo Comics"; + PublisherImpl publisherImpl = new PublisherImpl(); + publisherImpl.addPublisher("Bongo Comics"); + publisherImpl.addPublisher("Marvel"); + Collection publisherList = publisherImpl.findAll(); + Assert.assertEquals(2, publisherList.size()); + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/ArtistImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/ArtistImplTest.java new file mode 100644 index 0000000..4095fba --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/ArtistImplTest.java @@ -0,0 +1,50 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ArtistEntity; +import com.ibtp.kontor.dal.*; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.*; + +import java.util.Collection; +import java.util.Iterator; + +public class ArtistImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testArtistAddAndDelete() { + String artistName = "testArtistAddAndDelete"; + ArtistImpl artistImpl = new ArtistImpl(); + artistImpl.addArtist(artistName); + Collection resultList = artistImpl.findByName(artistName); + Assert.assertNotNull(resultList); + Assert.assertTrue(resultList.size() > 0); + ArtistEntity artist = (ArtistEntity)(resultList.toArray()[0]); + artistImpl.delete(artist); + resultList = artistImpl.findByName(artistName); + Assert.assertNotNull(resultList); + Assert.assertEquals(0, resultList.size()); + } + + @Test + public void testArtistFindAll() { + ArtistImpl artistImpl = new ArtistImpl(); + Collection artistList = artistImpl.findAll(); + Assert.assertNotNull(artistList); + Assert.assertEquals(0, artistList.size()); + artistImpl.addArtist("testArtistFindAll1"); + artistImpl.addArtist("testArtistFindAll2"); + artistImpl.addArtist("testArtistFindAll3"); + artistList = artistImpl.findAll(); + Assert.assertNotNull(artistList); + Assert.assertEquals(3, artistList.size()); + for (Iterator iterator = artistList.iterator(); iterator.hasNext(); ) { + ArtistEntity next = iterator.next(); + artistImpl.delete(next); + } + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/ComicImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/ComicImplTest.java new file mode 100644 index 0000000..eb494c9 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/ComicImplTest.java @@ -0,0 +1,54 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ComicEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class ComicImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testComicAddAndDelete() { + String comicTitle = "Comic1"; + ComicImpl comicImpl = new ComicImpl(); + comicImpl.addComic(comicTitle); + Collection comicList = comicImpl.findByTitle(comicTitle); + Assert.assertNotNull(comicList); + Assert.assertEquals(1, comicList.size()); + comicImpl.delete((ComicEntity) comicList.toArray()[0]); + comicList = comicImpl.findByTitle(comicTitle); + Assert.assertNotNull(comicList); + Assert.assertEquals(0, comicList.size()); + } + + @Test + public void testComicFindAll() { + ComicImpl comicImpl = new ComicImpl(); + comicImpl.addComic("Comic1"); + comicImpl.addComic("Comic2"); + comicImpl.addComic("Comic3"); + Collection comicList = comicImpl.findAll(); + Assert.assertNotNull(comicList); + Assert.assertEquals(3, comicList.size()); + for (Iterator iterator = comicList.iterator(); iterator.hasNext(); ) { + ComicEntity next = iterator.next(); + comicImpl.delete(next); + } + comicList = comicImpl.findAll(); + Assert.assertNotNull(comicList); + Assert.assertEquals(0, comicList.size()); + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/IssueImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/IssueImplTest.java new file mode 100644 index 0000000..4cf7cdc --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/IssueImplTest.java @@ -0,0 +1,62 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.IssueEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class IssueImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testIssueAddAndDelete() { + String issueNumber = "42"; + IssueImpl issueImpl = new IssueImpl(); + IssueEntity issue = new IssueEntity(); + issue.setNumber(issueNumber); + issueImpl.store(issue); + Collection issueList = issueImpl.findByNumber(issueNumber); + Assert.assertNotNull(issueList); + Assert.assertEquals(1, issueList.size()); + issueImpl.delete(issue); + issueList = issueImpl.findByNumber(issueNumber); + Assert.assertNotNull(issueList); + Assert.assertEquals(0, issueList.size()); + } + + @Test + public void testIssueFindAll() { + IssueImpl issueImpl = new IssueImpl(); + IssueEntity issue1 = new IssueEntity(); + issue1.setNumber("issue1"); + IssueEntity issue2 = new IssueEntity(); + issue1.setNumber("issue2"); + IssueEntity issue3 = new IssueEntity(); + issue1.setNumber("issue3"); + issueImpl.store(issue1); + issueImpl.store(issue2); + issueImpl.store(issue3); + Collection issueList = issueImpl.findAll(); + Assert.assertNotNull(issueList); + Assert.assertEquals(3, issueList.size()); + for (Iterator iterator = issueList.iterator(); iterator.hasNext(); ) { + IssueEntity next = iterator.next(); + issueImpl.delete(next); + } + issueList = issueImpl.findAll(); + Assert.assertNotNull(issueList); + Assert.assertEquals(0, issueList.size()); + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/PublisherImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/PublisherImplTest.java new file mode 100644 index 0000000..9f459cc --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/PublisherImplTest.java @@ -0,0 +1,50 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.PublisherEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 20.01.2015. + */ +public class PublisherImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testPublisherAddAndDelete() { + String publisherName = "testPublisherAddAndDelete"; + PublisherImpl publisherImpl = new PublisherImpl(); + PublisherEntity publisher = publisherImpl.addPublisher(publisherName); + Collection publisherList = publisherImpl.findByName(publisherName); + Assert.assertEquals(1, publisherList.size()); + publisherImpl.delete(publisher); + publisherList = publisherImpl.findByName(publisherName); + Assert.assertEquals(0, publisherList.size()); + } + + @Test + public void testPublisherFindAll() { + PublisherImpl publisherImpl = new PublisherImpl(); + publisherImpl.addPublisher("testDeletePublisher1"); + publisherImpl.addPublisher("testDeletePublisher2"); + publisherImpl.addPublisher("testDeletePublisher3"); + Collection publisherList = publisherImpl.findAll(); + Assert.assertEquals(3, publisherList.size()); + for (Iterator iterator = publisherList.iterator(); iterator.hasNext(); ) { + PublisherEntity next = iterator.next(); + publisherImpl.delete(next); + } + publisherList = publisherImpl.findAll(); + Assert.assertEquals(0, publisherList.size()); + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/StoryArcImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/StoryArcImplTest.java new file mode 100644 index 0000000..8df7b94 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/StoryArcImplTest.java @@ -0,0 +1,55 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.StoryArcEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collection; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class StoryArcImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testStoryArcAddAndDelete() { + String storyArcTtitle = "testStoryArcAddAndDelete"; + StoryArcImpl storyArcImpl = new StoryArcImpl(); + StoryArcEntity storyArc = new StoryArcEntity(); + storyArc.setTitle(storyArcTtitle); + storyArcImpl.store(storyArc); + Collection storyArcEntityCollection = storyArcImpl.findByTitle(storyArcTtitle); + Assert.assertNotNull(storyArcEntityCollection); + Assert.assertEquals(1, storyArcEntityCollection.size()); + storyArcImpl.delete(storyArc); + storyArcEntityCollection = storyArcImpl.findByTitle(storyArcTtitle); + Assert.assertNotNull(storyArcEntityCollection); + Assert.assertEquals(0, storyArcEntityCollection.size()); + } + + @Test + public void testStoryArcFindAll() { + StoryArcImpl storyArcImpl = new StoryArcImpl(); + StoryArcEntity storyArc; + storyArc = new StoryArcEntity(); + storyArc.setTitle("testStoryArcFindAll1"); + storyArcImpl.store(storyArc); + storyArc = new StoryArcEntity(); + storyArc.setTitle("testStoryArcFindAll2"); + storyArcImpl.store(storyArc); + storyArc = new StoryArcEntity(); + storyArc.setTitle("testStoryArcFindAll3"); + storyArcImpl.store(storyArc); + Collection storyArcEntityCollection = storyArcImpl.findAll(); + Assert.assertNotNull(storyArcEntityCollection); + Assert.assertEquals(3, storyArcEntityCollection.size()); + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/VolumeImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/VolumeImplTest.java new file mode 100644 index 0000000..e5c5b3a --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/VolumeImplTest.java @@ -0,0 +1,67 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.VolumeEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class VolumeImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testVolumeAddAndDelete() { + String volumeTitle = "testVolumeAddAndDelete"; + VolumeImpl volumeImpl = new VolumeImpl(); + VolumeEntity volume = new VolumeEntity(); + volume.setTitle(volumeTitle); + VolumeEntity volumeEntity = volumeImpl.store(volume); + Assert.assertNotNull(volumeEntity); + Assert.assertEquals(volumeTitle, volumeEntity.getTitle()); + Collection volumeList = volumeImpl.findByTitle(volumeTitle); + Assert.assertNotNull(volumeList); + Assert.assertEquals(1, volumeList.size()); + VolumeEntity result = (VolumeEntity)volumeList.toArray()[0]; + Assert.assertEquals(volume, result); + volumeImpl.delete(result); + volumeList = volumeImpl.findByTitle(volumeTitle); + Assert.assertEquals(0, volumeList.size()); + } + + @Test + public void testVolumeFindAll() { + VolumeImpl volumeImpl = new VolumeImpl(); + VolumeEntity volume; + volume = new VolumeEntity(); + volume.setTitle("testVolumeFindAll1"); + volumeImpl.store(volume); + volume = new VolumeEntity(); + volume.setTitle("testVolumeFindAll2"); + volumeImpl.store(volume); + volume = new VolumeEntity(); + volume.setTitle("testVolumeFindAll3"); + volumeImpl.store(volume); + Collection volumeList = volumeImpl.findAll(); + Assert.assertNotNull(volumeList); + Assert.assertEquals(3, volumeList.size()); + for (Iterator iterator = volumeList.iterator(); iterator.hasNext(); ) { + VolumeEntity next = iterator.next(); + volumeImpl.delete(next); + } + volumeList = volumeImpl.findAll(); + Assert.assertNotNull(volumeList); + Assert.assertEquals(0, volumeList.size()); + } + +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/dal/DataAccessLayerTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/dal/DataAccessLayerTest.java new file mode 100644 index 0000000..6351468 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/dal/DataAccessLayerTest.java @@ -0,0 +1,62 @@ +package com.ibtp.kontor.dal; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Method; + +/** + * Created by TPEETZ on 10.02.2015. + */ +public class DataAccessLayerTest { + + public void findTests(String packageName, String entityName) { + String testClassName = packageName + entityName + "ImplTest"; + Class testClass; + try { + testClass = Class.forName(testClassName); + Method addAndDelete = testClass.getMethod("test" + entityName + "AddAndDelete"); + Method findAll = testClass.getMethod("test" + entityName + "FindAll"); + } catch (ClassNotFoundException e) { + Assert.fail("Class " + testClassName + " missing"); + } catch (NoSuchMethodException e) { + Assert.fail("Test method for class " + testClassName + " missing"); + } + } + + @Test + public void testFindComicTests() { + /* + * Find all Tests + */ + String[] testClasses = new String[]{"Artist", "Comic", "Issue", "Publisher", "StoryArc", "Volume"}; + for (int i = 0; i < testClasses.length; i++) { + String testEntity = testClasses[i]; + findTests("com.ibtp.kontor.comics.dal.", testEntity); + } + } + + @Test + public void testFindLibraryTests() { + /* + * Find all Tests + */ + String[] testClasses = new String[]{"Article", "Author", "Book", "File", "Title"}; + for (int i = 0; i < testClasses.length; i++) { + String testEntity = testClasses[i]; + findTests("com.ibtp.kontor.library.dal.", testEntity); + } + } + + @Test + public void testFindTradingCardsTests() { + /* + * Find all Tests + */ + String[] testClasses = new String[]{"BaseSet", "Insert", "Manufacturer", "ParallelSet", "Player", "Position", "SportCard", "Sport", "Team"}; + for (int i = 0; i < testClasses.length; i++) { + String testEntity = testClasses[i]; + findTests("com.ibtp.kontor.tradingcards.dal." , testEntity); + } + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/BookshelfTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/BookshelfTest.java new file mode 100644 index 0000000..57f0a84 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/BookshelfTest.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.library; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Before; +import org.junit.Test; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class BookshelfTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testAddAuthors() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/ArticleImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/ArticleImplTest.java new file mode 100644 index 0000000..5fa268b --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/ArticleImplTest.java @@ -0,0 +1,41 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import com.ibtp.kontor.library.entity.ArticleEntity; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +public class ArticleImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testAddArticle() { + String articleTitle = "testAddArticle"; + ArticleImpl articleImpl = new ArticleImpl(); + ArticleEntity article = articleImpl.addArticle(articleTitle); + Assert.assertNotNull(article); + List articleList = articleImpl.findByTitle(articleTitle); + Assert.assertEquals(1, articleList.size()); + } + + @Test + public void testArticleAddAndDelete() { + + } + + @Test + public void testArticleFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/AuthorImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/AuthorImplTest.java new file mode 100644 index 0000000..f10caf6 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/AuthorImplTest.java @@ -0,0 +1,52 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import com.ibtp.kontor.library.entity.AuthorEntity; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +/** + * Created by thomas on 23.01.15. + */ +public class AuthorImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testAddAuthor() { + String authorName = "testAddAuthor"; + AuthorImpl authorImpl = new AuthorImpl(); + AuthorEntity author = authorImpl.addAuthor(authorName); + Assert.assertNotNull(author); + } + + @Test + public void testDeleteAuthor() { + String authorName = "testDeleteAuthor"; + AuthorImpl authorImpl = new AuthorImpl(); + AuthorEntity author = authorImpl.addAuthor(authorName); + Assert.assertNotNull(author); + List authorList = authorImpl.findByName(authorName); + Assert.assertEquals(1, authorList.size()); + authorImpl.delete(author); + authorList = authorImpl.findByName(authorName); + Assert.assertEquals(0, authorList.size()); + } + + @Test + public void testAuthorAddAndDelete() { + + } + + @Test + public void testAuthorFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/BookImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/BookImplTest.java new file mode 100644 index 0000000..ba4c6d4 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/BookImplTest.java @@ -0,0 +1,27 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Before; +import org.junit.Test; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class BookImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testBookAddAndDelete() { + + } + + @Test + public void testBookFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/FileImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/FileImplTest.java new file mode 100644 index 0000000..a0afab6 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/FileImplTest.java @@ -0,0 +1,27 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Before; +import org.junit.Test; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class FileImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testFileAddAndDelete() { + + } + + @Test + public void testFileFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/TitleImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/TitleImplTest.java new file mode 100644 index 0000000..e6f06bd --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/TitleImplTest.java @@ -0,0 +1,27 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Before; +import org.junit.Test; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class TitleImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testTitleAddAndDelete() { + + } + + @Test + public void testTitleFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/CollectionTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/CollectionTest.java new file mode 100644 index 0000000..d307051 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/CollectionTest.java @@ -0,0 +1,167 @@ +package com.ibtp.kontor.tradingcards; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.tradingcards.dal.TeamImpl; +import com.ibtp.kontor.tradingcards.entity.TeamEntity; +import com.ibtp.kontor.util.LocalTestDatabase; +import com.ibtp.kontor.tradingcards.dal.SportImpl; +import com.ibtp.kontor.tradingcards.entity.SportEntity; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class CollectionTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + setupSports(); + } + + public void setupSports() { + SportImpl sportImpl = new SportImpl(); + SportEntity football = sportImpl.addSport("Football"); + setupFootballTeams(football); + SportEntity baseball = sportImpl.addSport("Baseball"); + setupFootballTeams(baseball); + SportEntity basketball = sportImpl.addSport("Basketball"); + setupBasketballTeams(basketball); + SportEntity hockey = sportImpl.addSport("Hockey"); + setupHockeyTeams(hockey); + } + + public void setupFootballTeams(SportEntity football) { + TeamImpl teamImpl = new TeamImpl(); + teamImpl.addTeam("Dallas Cowboys", football); + teamImpl.addTeam("New York Giants", football); + teamImpl.addTeam("Philadelphia Eagles", football); + teamImpl.addTeam("Arizona Cardinals", football); + teamImpl.addTeam("Washington Redskins", football); + teamImpl.addTeam("Detroit Lions", football); + teamImpl.addTeam("Minnesota Vikings", football); + teamImpl.addTeam("Green Bay Packers", football); + teamImpl.addTeam("Chicago Bears", football); + teamImpl.addTeam("Tampa Bay Buccaneers", football); + teamImpl.addTeam("San Francisco 49ers", football); + teamImpl.addTeam("New Orleans Saints", football); + teamImpl.addTeam("Atlanta Falcons", football); + teamImpl.addTeam("Los Angeles Rams", football); + teamImpl.addTeam("Buffalo Bills", football); + teamImpl.addTeam("Miami Dolphins", football); + teamImpl.addTeam("New York Jets", football); + teamImpl.addTeam("New England Patriots", football); + teamImpl.addTeam("Indianapolis Colts", football); + teamImpl.addTeam("Houston Oilers", football); + teamImpl.addTeam("Pittsburgh Steelers", football); + teamImpl.addTeam("Cleveland Browns", football); + teamImpl.addTeam("Kansas City Chiefs", football); + teamImpl.addTeam("Los Angeles Raiders", football); + teamImpl.addTeam("Denver Broncos", football); + teamImpl.addTeam("San Diego Chargers", football); + teamImpl.addTeam("Seattle Seahawks", football); + teamImpl.addTeam("Jacksonville Jaguars", football); + teamImpl.addTeam("Houston Texans", football); + } + + public void setupBaseballTeams(SportEntity baseball) { + TeamImpl teamImpl = new TeamImpl(); + } + + public void setupBasketballTeams(SportEntity basketball) { + TeamImpl teamImpl = new TeamImpl(); + teamImpl.addTeam("Houston Rockets", basketball); + teamImpl.addTeam("San Antonio Spurs", basketball); + teamImpl.addTeam("Utah Jazz", basketball); + teamImpl.addTeam("Denver Nuggets", basketball); + teamImpl.addTeam("Minnesota Timberwolves", basketball); + teamImpl.addTeam("Dallas Mavericks", basketball); + teamImpl.addTeam("Seattle SuperSonics", basketball); + teamImpl.addTeam("Phoenix Suns", basketball); + teamImpl.addTeam("Golden State Warriors", basketball); + teamImpl.addTeam("Portland Trail Blazers", basketball); + teamImpl.addTeam("Los Angeles Lakers", basketball); + teamImpl.addTeam("Sacramento Kings", basketball); + teamImpl.addTeam("Los Angeles Clippers", basketball); + teamImpl.addTeam("New York Knicks", basketball); + teamImpl.addTeam("Orlando Magic", basketball); + teamImpl.addTeam("New Jersey Nets", basketball); + teamImpl.addTeam("Miami Heat", basketball); + teamImpl.addTeam("Boston Celtics", basketball); + teamImpl.addTeam("Philadelphia 76ers", basketball); + teamImpl.addTeam("Washington Bullets", basketball); + teamImpl.addTeam("Atlanta Hawks", basketball); + teamImpl.addTeam("Chicago Bulls", basketball); + teamImpl.addTeam("Indiana Pacers", basketball); + teamImpl.addTeam("Cleveland Cavaliers", basketball); + teamImpl.addTeam("Charlotte Hornets", basketball); + teamImpl.addTeam("Detroit Pistons", basketball); + teamImpl.addTeam("Milwaukee Bucks", basketball); + } + + public void setupHockeyTeams(SportEntity hockey) { + TeamImpl teamImpl = new TeamImpl(); + teamImpl.addTeam("New York Rangers", hockey); + teamImpl.addTeam("Buffalo Sabers", hockey); + teamImpl.addTeam("Detroit Red Wings", hockey); + teamImpl.addTeam("Vancouver Canucks", hockey); + teamImpl.addTeam("Mighty Ducks of Anaheim", hockey); + teamImpl.addTeam("Calgary Flames", hockey); + teamImpl.addTeam("Edmonton Oilers", hockey); + teamImpl.addTeam("Los Angeles Kings", hockey); + teamImpl.addTeam("San Jose Sharks", hockey); + teamImpl.addTeam("Chicago Blackhawks", hockey); + teamImpl.addTeam("Dallas Stars", hockey); + teamImpl.addTeam("St. Louis Blues", hockey); + teamImpl.addTeam("Toronto Maple Leafs", hockey); + teamImpl.addTeam("Winnipeg Jets", hockey); + teamImpl.addTeam("Boston Bruins", hockey); + teamImpl.addTeam("Hartford Whalers", hockey); + teamImpl.addTeam("Montreal Canadiers", hockey); + teamImpl.addTeam("Ottawa Senators", hockey); + teamImpl.addTeam("Pittsburgh Penguins", hockey); + teamImpl.addTeam("Quebec Nordiques", hockey); + teamImpl.addTeam("Florida Panthers", hockey); + teamImpl.addTeam("New Jersey Devils", hockey); + teamImpl.addTeam("New York Islanders", hockey); + teamImpl.addTeam("Philadelphia Flyers", hockey); + teamImpl.addTeam("Tamba Bay Lightning", hockey); + teamImpl.addTeam("Washington Capitals", hockey); + } + + @After + public void tearDown() { + TeamImpl teamImpl = new TeamImpl(); + Collection teamEntities = teamImpl.findAll(); + for (Iterator iterator = teamEntities.iterator(); iterator.hasNext(); ) { + TeamEntity next = iterator.next(); + teamImpl.delete(next); + } + SportImpl sportImpl = new SportImpl(); + Collection sportEntities = sportImpl.findAll(); + for (Iterator iterator = sportEntities.iterator(); iterator.hasNext(); ) { + SportEntity next = iterator.next(); + sportImpl.delete(next); + } + } + + @Test + public void gettAllSports() { + SportImpl sportImpl = new SportImpl(); + Collection resultList = sportImpl.findAll(); + Assert.assertEquals(4, resultList.size()); + } + + @Test + public void getAllTeams() { + TeamImpl teamImpl = new TeamImpl(); + Collection resultList = teamImpl.findAll(); + Assert.assertEquals(111, resultList.size()); + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/BaseSetImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/BaseSetImplTest.java new file mode 100644 index 0000000..00785cc --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/BaseSetImplTest.java @@ -0,0 +1,27 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Before; +import org.junit.Test; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class BaseSetImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testBaseSetAddAndDelete() { + + } + + @Test + public void testBaseSetFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/InsertImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/InsertImplTest.java new file mode 100644 index 0000000..c7fe97a --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/InsertImplTest.java @@ -0,0 +1,28 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Before; +import org.junit.Test; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class InsertImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + + @Test + public void testInsertAddAndDelete() { + + } + + @Test + public void testInsertFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImplTest.java new file mode 100644 index 0000000..972aa84 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImplTest.java @@ -0,0 +1,52 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import com.ibtp.kontor.tradingcards.entity.ManufacturerEntity; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +/** + * Created by tpeetz on 20.01.2015. + */ +public class ManufacturerImplTest { + + @Before + public void setup() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void addManufacturer() { + String manufacturerName = "Manufacturer1"; + ManufacturerImpl manufacturerImpl = new ManufacturerImpl(); + ManufacturerEntity manufacturer = manufacturerImpl.addManufacturer(manufacturerName); + Assert.assertNotNull(manufacturer); + List manufacturerList = manufacturerImpl.findByName(manufacturerName); + Assert.assertTrue(manufacturerList.size() > 0); + } + + @Test + public void deleteManufacturer() { + String manufacturerName = "Manufacturer1"; + ManufacturerImpl manufacturerImpl = new ManufacturerImpl(); + List manufacturerList = manufacturerImpl.findByName(manufacturerName); + Assert.assertTrue(manufacturerList.size() > 0); + manufacturerImpl.delete(manufacturerList.get(0)); + manufacturerList = manufacturerImpl.findByName(manufacturerName); + Assert.assertEquals(0, manufacturerList.size()); + } + + @Test + public void testManufacturerAddAndDelete() { + + } + + @Test + public void testManufacturerFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImplTest.java new file mode 100644 index 0000000..19cc143 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImplTest.java @@ -0,0 +1,27 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Before; +import org.junit.Test; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class ParallelSetImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testParallelSetAddAndDelete() { + + } + + @Test + public void testParallelSetFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/PlayerImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/PlayerImplTest.java new file mode 100644 index 0000000..6aca0db --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/PlayerImplTest.java @@ -0,0 +1,27 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Before; +import org.junit.Test; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class PlayerImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testPlayerAddAndDelete() { + + } + + @Test + public void testPlayerFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/PositionImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/PositionImplTest.java new file mode 100644 index 0000000..3d2019b --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/PositionImplTest.java @@ -0,0 +1,42 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import com.ibtp.kontor.tradingcards.entity.PositionEntity; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collection; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class PositionImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testPositionAddAndDelete() { + String positionName = "testPositionAddAndDelete"; + PositionImpl positionImpl = new PositionImpl(); + PositionEntity position = positionImpl.addPosition(positionName); + Collection resultList = positionImpl.findByName(positionName); + Assert.assertNotNull(resultList); + Assert.assertEquals(1, resultList.size()); + positionImpl.delete(position); + resultList = positionImpl.findByName(positionName); + Assert.assertEquals(0, resultList.size()); + } + + @Test + public void testPositionFindAll() { + PositionImpl positionImpl = new PositionImpl(); + Collection resultList = positionImpl.findAll(); + Assert.assertEquals(0, resultList.size()); + } +} + diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/SportCardImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/SportCardImplTest.java new file mode 100644 index 0000000..4e9d190 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/SportCardImplTest.java @@ -0,0 +1,27 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Before; +import org.junit.Test; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class SportCardImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testSportCardAddAndDelete() { + + } + + @Test + public void testSportCardFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/SportImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/SportImplTest.java new file mode 100644 index 0000000..f8053c5 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/SportImplTest.java @@ -0,0 +1,41 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import com.ibtp.kontor.tradingcards.entity.SportEntity; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +public class SportImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testSportAddAndDelete() { + String sportName = "testSportAddAndDelete"; + SportImpl sportImpl = new SportImpl(); + SportEntity sport = sportImpl.addSport(sportName); + List sportList = sportImpl.findByName(sportName); + Assert.assertEquals(1, sportList.size()); + sportImpl.delete(sport); + List result = sportImpl.findByName(sportName); + Assert.assertEquals(0, result.size()); + } + + @Test + public void testSportFindAll() { + SportImpl sportImpl = new SportImpl(); + Collection resultList = sportImpl.findAll(); + Assert.assertEquals(0, resultList.size()); + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/TeamImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/TeamImplTest.java new file mode 100644 index 0000000..1cfec7b --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/TeamImplTest.java @@ -0,0 +1,41 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import com.ibtp.kontor.tradingcards.entity.TeamEntity; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collection; + +/** + * Created by tpeetz on 20.01.2015. + */ +public class TeamImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testTeamAddAndDelete() { + String teamName = "testTeamAddAndDelete"; + TeamEntity team = new TeamEntity(teamName); + TeamImpl teamImpl = new TeamImpl(); + teamImpl.store(team); + Collection resultList = teamImpl.findByName(teamName); + Assert.assertEquals(1, resultList.size()); + teamImpl.delete(team); + resultList = teamImpl.findByName(teamName); + Assert.assertEquals(0, resultList.size()); + } + + @Test + public void testTeamFindAll() { + TeamImpl teamImpl = new TeamImpl(); + Collection resultList = teamImpl.findAll(); + Assert.assertEquals(0, resultList.size()); + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/util/LocalTestDatabase.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/util/LocalTestDatabase.java new file mode 100644 index 0000000..503104a --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/util/LocalTestDatabase.java @@ -0,0 +1,110 @@ +package com.ibtp.kontor.util; + +import com.ibtp.kontor.dal.Database; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hsqldb.Server; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import javax.persistence.spi.PersistenceProvider; +import javax.persistence.spi.PersistenceProviderResolver; +import javax.persistence.spi.PersistenceProviderResolverHolder; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Created by TPEETZ on 21.01.2015. + */ +public class LocalTestDatabase implements Database { + + private static Server server; + private static EntityManagerFactory factory; + private static EntityManager em; + private static Logger logger = LoggerFactory.getLogger(LocalTestDatabase.class.getName()); + + static { + logger.info("initialization and starting database"); + LocalTestDatabase.assureDatabaseRunning(); + } + + public LocalTestDatabase() { + logger.info("LocalDatabaseTest started"); + } + + private static void assureDatabaseRunning() { + if (LocalTestDatabase.server == null) { + LocalTestDatabase.startDatabase(); + } + } + + private static void startDatabase() { + logger.info("startDatabase as kontor in hsqldb_databases/test"); + LocalTestDatabase.server = new Server(); + LocalTestDatabase.server.setAddress("localhost"); + LocalTestDatabase.server.setDatabaseName(0, "kontor"); + LocalTestDatabase.server.setDatabasePath(0, "file:build/hsqldb_databases/test"); + LocalTestDatabase.server.setPort(2345); + LocalTestDatabase.server.setTrace(true); + LocalTestDatabase.server.setLogWriter(new PrintWriter(System.out)); + LocalTestDatabase.server.start(); + } + + private static void stopDatabase() { + server.shutdown(); + } + + private static EntityManagerFactory getFactory() { + if (LocalTestDatabase.factory == null) { + LocalTestDatabase.assureDatabaseRunning(); + PersistenceProviderResolverHolder.setPersistenceProviderResolver(new PersistenceProviderResolver() { + private final List providers_ = Arrays.asList((PersistenceProvider) new HibernatePersistenceProvider()); + + @Override + public void clearCachedProviders() { + // Auto-generated method stub + } + + @Override + public List getPersistenceProviders() { + return providers_; + } + }); + LocalTestDatabase.factory = Persistence.createEntityManagerFactory("com.ibtp.kontor"); + logger.info("EntityManagerFactory(com.ibtp.kontor) created"); + } + return factory; + } + + private static EntityManager getSingleEntityManager() { + return LocalTestDatabase.em; + } + + private static void setSingleEntityManager(EntityManager manager) { + LocalTestDatabase.em = manager; + } + + @Override + public EntityManager getEntityManager() { + if (getSingleEntityManager() == null) { + setSingleEntityManager(getFactory().createEntityManager()); + logger.info("EntityManager created"); + } + return getSingleEntityManager(); + } + + @Override + public String toString() { + String serverMessage; + if (LocalTestDatabase.server == null) { + serverMessage = "server:null"; + } else { + serverMessage = LocalTestDatabase.server.toString(); + } + return LocalTestDatabase.class.getName() + " " + serverMessage; + } +} diff --git a/java-ee/KontorApp/src/test/resources/META-INF/persistence.xml b/java-ee/KontorApp/src/test/resources/META-INF/persistence.xml new file mode 100644 index 0000000..35d671b --- /dev/null +++ b/java-ee/KontorApp/src/test/resources/META-INF/persistence.xml @@ -0,0 +1,39 @@ + + + org.hibernate.jpa.HibernatePersistenceProvider + com.ibtp.kontor.comics.entity.ArtistEntity + com.ibtp.kontor.comics.entity.ComicEntity + com.ibtp.kontor.comics.entity.IssueEntity + com.ibtp.kontor.comics.entity.StoryArcEntity + com.ibtp.kontor.comics.entity.VolumeEntity + com.ibtp.kontor.comics.entity.PublisherEntity + com.ibtp.kontor.library.entity.AuthorEntity + com.ibtp.kontor.library.entity.ArticleEntity + com.ibtp.kontor.library.entity.BookEntity + com.ibtp.kontor.library.entity.FileEntity + com.ibtp.kontor.library.entity.TitleEntity + com.ibtp.kontor.tradingcards.entity.SportEntity + com.ibtp.kontor.tradingcards.entity.TeamEntity + com.ibtp.kontor.tradingcards.entity.PositionEntity + com.ibtp.kontor.tradingcards.entity.PlayerEntity + com.ibtp.kontor.tradingcards.entity.ManufacturerEntity + com.ibtp.kontor.tradingcards.entity.BaseSetEntity + com.ibtp.kontor.tradingcards.entity.InsertEntity + com.ibtp.kontor.tradingcards.entity.ParallelSetEntity + com.ibtp.kontor.tradingcards.entity.SportCardEntity + + + + + + + + + + + + + diff --git a/java-ee/KontorApp/src/test/resources/logback.xml b/java-ee/KontorApp/src/test/resources/logback.xml new file mode 100644 index 0000000..5254e50 --- /dev/null +++ b/java-ee/KontorApp/src/test/resources/logback.xml @@ -0,0 +1,41 @@ + + + + + + %d{yyyy-MM-dd_HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + + + + + build/kontortest.log + + %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + build/kontortest.%i.log.zip + 1 + 10 + + + + 2MB + + + + + + + + + + + + + + diff --git a/java-ee/KontorEJB/build.gradle b/java-ee/KontorEJB/build.gradle new file mode 100755 index 0000000..5f1dfc0 --- /dev/null +++ b/java-ee/KontorEJB/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'java' + +dependencies { + compile 'javax.enterprise:cdi-api:+' + compile 'org.jboss.spec.javax.faces:jboss-jsf-api_2.2_spec:+' + compile 'org.jboss.spec.javax.ejb:jboss-ejb-api_3.2_spec:+' + compile 'org.jboss.spec.javax.annotation:jboss-annotations-api_1.2_spec:+' + compile 'org.hibernate.ogm:hibernate-ogm-mongodb:+' + compile 'org.eclipse.persistence:javax.persistence:2.1.0' + compile 'ch.qos.logback:logback-core:1.1.2' + compile 'ch.qos.logback:logback-classic:1.1.2' +} diff --git a/java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/Controller.java b/java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/Controller.java new file mode 100755 index 0000000..67b84ce --- /dev/null +++ b/java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/Controller.java @@ -0,0 +1,62 @@ +package com.ibtp.kontor.ejb; + +import com.ibtp.kontor.ejb.PropertyManager; +import com.ibtp.kontor.ejb.Property; + +import java.util.List; +import javax.annotation.PostConstruct; +import javax.enterprise.inject.Model; + +import javax.inject.Inject; + +@Model +public class Controller { + + List propertyList; + + private String key; + private String value; + + @PostConstruct + public void readDB() { + propertyList = ejb.queryCache(); + + } + @Inject + PropertyManager ejb; + + public void save() { + Property p = new Property(); + p.setKey(key); + p.setValue(value); + ejb.save(p); + propertyList.add(p); + key = ""; + value = ""; + } + + public List getPropertyList() { + return propertyList; + } + + public void setPropertyList(List propertyList) { + this.propertyList = propertyList; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + +} \ No newline at end of file diff --git a/java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/Property.java b/java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/Property.java new file mode 100755 index 0000000..d2fab7b --- /dev/null +++ b/java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/Property.java @@ -0,0 +1,37 @@ +package com.ibtp.kontor.ejb; + + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import org.hibernate.annotations.GenericGenerator; + +@Entity +public class Property { + + @Id + @GeneratedValue(generator = "uuid") + @GenericGenerator(name = "uuid", strategy = "uuid2") + private String id; + + private String key; + + private String value; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + +} \ No newline at end of file diff --git a/java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/PropertyManager.java b/java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/PropertyManager.java new file mode 100755 index 0000000..865a0bd --- /dev/null +++ b/java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/PropertyManager.java @@ -0,0 +1,28 @@ +package com.ibtp.kontor.ejb; + +import java.util.List; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +import com.ibtp.kontor.ejb.Property; +import javax.ejb.Stateless; + +@Stateless + public class PropertyManager { + + @PersistenceContext(unitName = "mongo-ogm") + private EntityManager em; + + public void save(Property p) { + em.persist(p); + } + + public List queryCache() { + Query query = em.createQuery("FROM Property p"); + + List list = query.getResultList(); + return list; + } +} \ No newline at end of file diff --git a/java-ee/KontorImpl/build.gradle b/java-ee/KontorImpl/build.gradle new file mode 100644 index 0000000..b1512a8 --- /dev/null +++ b/java-ee/KontorImpl/build.gradle @@ -0,0 +1,5 @@ +jar { + manifest { + attributes 'Implementation-Title': 'Kontor', 'Implementation-Version': version + } +} diff --git a/java-ee/KontorImpl/config/checkstyle/checkstyle.xml b/java-ee/KontorImpl/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..7c682c3 --- /dev/null +++ b/java-ee/KontorImpl/config/checkstyle/checkstyle.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java-ee/KontorImpl/config/checkstyle/checkstyle.xsl b/java-ee/KontorImpl/config/checkstyle/checkstyle.xsl new file mode 100644 index 0000000..393a01b --- /dev/null +++ b/java-ee/KontorImpl/config/checkstyle/checkstyle.xsl @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

CheckStyle Audit

Designed for use with CheckStyle and Ant.
+
+ + + +
+ + + +
+ + + + + +

+

+ +


+ + + + +
+ + + + +

Files

+ + + + + + + + + + + + + + +
NameErrors
+
+ + + + +

File

+ + + + + + + + + + + + + +
Error DescriptionLine
+ Back to top +
+ + + +

Summary

+ + + + + + + + + + + + +
FilesErrors
+
+ + + + a + b + + +
+ + diff --git a/java-ee/KontorImpl/config/findbugs/findbugs.xml b/java-ee/KontorImpl/config/findbugs/findbugs.xml new file mode 100644 index 0000000..34a6e01 --- /dev/null +++ b/java-ee/KontorImpl/config/findbugs/findbugs.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java-ee/KontorImpl/src/main/java/com/peetz/kontor/dal/KontorUserDao.java b/java-ee/KontorImpl/src/main/java/com/peetz/kontor/dal/KontorUserDao.java new file mode 100644 index 0000000..f2c37f6 --- /dev/null +++ b/java-ee/KontorImpl/src/main/java/com/peetz/kontor/dal/KontorUserDao.java @@ -0,0 +1,16 @@ +package com.peetz.kontor.dal; + +import java.util.List; + +import javax.ejb.Local; + +import com.peetz.kontor.entity.KontorUserEntity; + +@Local +public interface KontorUserDao { + public KontorUserEntity getById(Long id); + public List findByIds(List ids); + public KontorUserEntity findByLogin(String login); + public KontorUserEntity store(KontorUserEntity entity); + public void delete(KontorUserEntity entity); +} diff --git a/java-ee/KontorImpl/src/main/java/com/peetz/kontor/dal/KontorUserImpl.java b/java-ee/KontorImpl/src/main/java/com/peetz/kontor/dal/KontorUserImpl.java new file mode 100644 index 0000000..3dee67f --- /dev/null +++ b/java-ee/KontorImpl/src/main/java/com/peetz/kontor/dal/KontorUserImpl.java @@ -0,0 +1,53 @@ +package com.peetz.kontor.dal; + +import java.util.List; + +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +import com.peetz.kontor.entity.KontorUserEntity; + +@Stateless(name = "ComicDao") +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class KontorUserImpl implements KontorUserDao { + + @PersistenceContext + private EntityManager em; + + @Override + public KontorUserEntity getById(Long id) { + Query q = em.createNamedQuery("findById"); + q.setParameter("id", id); + KontorUserEntity entity = (KontorUserEntity)q.getSingleResult(); + return entity; + } + + @Override + public List findByIds(List ids) { + // TODO Auto-generated method stub + return null; + } + + @Override + public KontorUserEntity findByLogin(String login) { + Query q = em.createNamedQuery("findByLogin"); + q.setParameter("login", login); + KontorUserEntity entity = (KontorUserEntity)q.getSingleResult(); + return entity; + } + + @Override + public KontorUserEntity store(KontorUserEntity entity) { + em.persist(entity); + return entity; + } + + @Override + public void delete(KontorUserEntity entity) { + em.remove(entity); + } +} diff --git a/java-ee/KontorImpl/src/main/java/com/peetz/kontor/entity/KontorUserEntity.java b/java-ee/KontorImpl/src/main/java/com/peetz/kontor/entity/KontorUserEntity.java new file mode 100644 index 0000000..5b8a0b5 --- /dev/null +++ b/java-ee/KontorImpl/src/main/java/com/peetz/kontor/entity/KontorUserEntity.java @@ -0,0 +1,62 @@ +package com.peetz.kontor.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="findAll", query="SELECT u from KontorUser as u"), + @NamedQuery(name="findById", query="SELECT u from KontorUser as u WHERE u.id = :id"), + @NamedQuery(name="findByLogin", query="SELECT u from KontorUser as u WHERE u.login = :login") +}) + +@Entity +@Table(name="KONTORUSER") +public class KontorUserEntity { + private Long id; + + private String login; + + private String password; + + private String name; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + @Column + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/java-ee/KontorImpl/src/main/java/com/peetz/kontor/service/package-info.java b/java-ee/KontorImpl/src/main/java/com/peetz/kontor/service/package-info.java new file mode 100644 index 0000000..e64dc61 --- /dev/null +++ b/java-ee/KontorImpl/src/main/java/com/peetz/kontor/service/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author TPEETZ + * + */ +package com.peetz.kontor.service; \ No newline at end of file diff --git a/java-ee/KontorWeb/build.gradle b/java-ee/KontorWeb/build.gradle new file mode 100755 index 0000000..e6eb2e0 --- /dev/null +++ b/java-ee/KontorWeb/build.gradle @@ -0,0 +1,11 @@ +apply plugin: 'war' + +version = '0.0.1' + +dependencies { + compile project(':KontorEJB') + compile project(':ComicsWeb') + compile project(':MedienWeb') + compile project(':LibraryWeb') + compile project(':TradingCardsWeb') +} diff --git a/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportComics.java b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportComics.java new file mode 100644 index 0000000..180eeda --- /dev/null +++ b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportComics.java @@ -0,0 +1,88 @@ +package com.peetz.kontor.data; + +import java.util.Collection; +import java.util.Iterator; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import com.peetz.comics.entity.ArtistEntity; +import com.peetz.comics.entity.ComicEntity; +import com.peetz.comics.entity.IssueEntity; +import com.peetz.comics.entity.PublisherEntity; +import com.peetz.comics.entity.StoryArcEntity; +import com.peetz.comics.service.ComicService; + +public class ExportComics +{ + public Element backupComics(Document document) + { + Element comics = document.createElement("comics"); + ComicService comicService = getComicService(); + if (comicService == null) return comics; + + Collection publishers = comicService.getAllPublisher(); + Iterator publisher_iterator = publishers.iterator(); + while (publisher_iterator.hasNext()) { + PublisherEntity publisher = publisher_iterator.next(); + Element publisherNode = document.createElement("publisher"); + publisherNode.setAttribute("id", publisher.getId().toString()); + publisherNode.setAttribute("name", publisher.getName()); + comics.appendChild(publisherNode); + comics.appendChild(document.createTextNode("\n")); + } + Collection artists = comicService.getAllArtists(); + Iterator artist_iterator = artists.iterator(); + while(artist_iterator.hasNext()) { + ArtistEntity artist = artist_iterator.next(); + Element artistNode = document.createElement("artist"); + artistNode.setAttribute("id", artist.getId().toString()); + artistNode.setAttribute("name", artist.getName()); + comics.appendChild(artistNode); + comics.appendChild(document.createTextNode("\n")); + } + Collection comicList = comicService.getAllComics(); + Iterator comics_iterator = comicList.iterator(); + while(comics_iterator.hasNext()) { + ComicEntity comic = comics_iterator.next(); + Element comicNode = document.createElement("comic"); + comicNode.setAttribute("id", comic.getId().toString()); + comicNode.setAttribute("title", comic.getTitle()); + String completed = "false"; + if (comic.getCompleted() != null) completed = comic.getCompleted().toString(); + comicNode.setAttribute("complete", completed); + String currentOrder = "false"; + if (comic.getCurrentOrder() != null) currentOrder = comic.getCurrentOrder().toString(); + comicNode.setAttribute("order", currentOrder); + Collection issues = comicService.getAllIssuesForComic(comic); + Iterator issues_iterator = issues.iterator(); + comicNode.appendChild(document.createTextNode("\n")); + while(issues_iterator.hasNext()) { + IssueEntity issue = issues_iterator.next(); + Element issueNode = document.createElement("issue"); + issueNode.setAttribute("id", issue.getId().toString()); + issueNode.setAttribute("number", issue.getNumber()); + comicNode.appendChild(issueNode); + comicNode.appendChild(document.createTextNode("\n")); + } + comics.appendChild(comicNode); + comics.appendChild(document.createTextNode("\n")); + } + Collection storyArcs = comicService.getAllStoryArcs(); + Iterator iterator_storyArcsIterator = storyArcs.iterator(); + while(iterator_storyArcsIterator.hasNext()) { + StoryArcEntity storyArc = iterator_storyArcsIterator.next(); + Element storyNode = document.createElement("storyArc"); + storyNode.setAttribute("id", storyArc.getId().toString()); + storyNode.setAttribute("title", storyArc.getTitle()); + comics.appendChild(storyNode); + comics.appendChild(document.createTextNode("\n")); + } + return comics; + } + + private ComicService getComicService() + { + return null; + } +} diff --git a/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportLibrary.java b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportLibrary.java new file mode 100644 index 0000000..1c17da9 --- /dev/null +++ b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportLibrary.java @@ -0,0 +1,100 @@ +package com.peetz.kontor.data; + +import java.util.Collection; +import java.util.Iterator; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import com.peetz.library.entity.ArticleEntity; +import com.peetz.library.entity.BookEntity; +import com.peetz.library.entity.BookshelfEntity; +import com.peetz.library.entity.ShelfboardEntity; +import com.peetz.library.service.LibraryService; + +public class ExportLibrary +{ + public Element backupLibrary(Document document) + { + Element library = document.createElement("library"); + library.appendChild(document.createTextNode("\n")); + LibraryService libraryService = getLibraryService(); + + Collection books = libraryService.getAllBooks(); + Iterator iterator_books = books.iterator(); + while(iterator_books.hasNext()) { + BookEntity book = iterator_books.next(); + Element bookNode = document.createElement("book"); + bookNode.setAttribute("id", book.getId().toString()); + bookNode.setAttribute("title", book.getTitle()); + bookNode.setAttribute("author", book.getAuthor()); + bookNode.setAttribute("edition", book.getEdition()); + bookNode.setAttribute("isbn", book.getIsbn()); + bookNode.setAttribute("pages", book.getPage().toString()); + bookNode.setAttribute("publisher", book.getPublisher()); + library.appendChild(bookNode); + library.appendChild(document.createTextNode("\n")); + } + + Collection bookshelfs = libraryService.getAllBookshelfs(); + Iterator iterator_bookshelfs = bookshelfs.iterator(); + while(iterator_bookshelfs.hasNext()) { + BookshelfEntity bookshelf = iterator_bookshelfs.next(); + Element shelfNode = document.createElement("shelf"); + shelfNode.setAttribute("id", bookshelf.getId().toString()); + shelfNode.setAttribute("title", bookshelf.getTitle()); + + shelfNode.appendChild(document.createTextNode("\n")); + Collection shelfboards = bookshelf.getShelfBoards(); + Iterator iterator_shelfboards = shelfboards.iterator(); + while(iterator_shelfboards.hasNext()) { + ShelfboardEntity shelfboard = iterator_shelfboards.next(); + Element boardNode = document.createElement("board"); + boardNode.setAttribute("id", shelfboard.getId().toString()); + boardNode.setAttribute("title", shelfboard.getTitle()); + shelfNode.appendChild(boardNode); + shelfNode.appendChild(document.createTextNode("\n")); + } + library.appendChild(shelfNode); + library.appendChild(document.createTextNode("\n")); + } + + Collection articles = libraryService.getAllArticles(); + Iterator iterator_articles = articles.iterator(); + while(iterator_articles.hasNext()) { + ArticleEntity article = iterator_articles.next(); + Element articleNode = document.createElement("article"); + articleNode.setAttribute("id", article.getId().toString()); + articleNode.setAttribute("title", article.getTitle()); + + Collection origins = article.getOriginArticles(); + Iterator iterator_origins = origins.iterator(); + while(iterator_origins.hasNext()) { + ArticleEntity origin = iterator_origins.next(); + Element originNode = document.createElement("article"); + originNode.setAttribute("id", origin.getId().toString()); + articleNode.appendChild(originNode); + articleNode.appendChild(document.createTextNode("\n")); + } + + Collection relateds = article.getRelatedArticles(); + Iterator iterator_relateds = relateds.iterator(); + articleNode.appendChild(document.createTextNode("\n")); + while(iterator_relateds.hasNext()) { + ArticleEntity related = iterator_relateds.next(); + Element relatedNode = document.createElement("article"); + relatedNode.setAttribute("id", related.getId().toString()); + articleNode.appendChild(relatedNode); + articleNode.appendChild(document.createTextNode("\n")); + } + library.appendChild(articleNode); + library.appendChild(document.createTextNode("\n")); + } + return library; + } + + private LibraryService getLibraryService() + { + return null; + } +} diff --git a/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportMedien.java b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportMedien.java new file mode 100644 index 0000000..48d0ee4 --- /dev/null +++ b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportMedien.java @@ -0,0 +1,75 @@ +package com.peetz.kontor.data; + +import java.util.Collection; +import java.util.Iterator; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import com.peetz.medien.entity.AudioCDEntity; +import com.peetz.medien.entity.BoxSetEntity; +import com.peetz.medien.entity.FilmEntity; +import com.peetz.medien.service.MedienService; + +public class ExportMedien +{ + public Element backupMedien(Document document) + { + Element medien = document.createElement("medien"); + medien.appendChild(document.createTextNode("\n")); + + MedienService medienService = getMedienService(); + + Collection cds = medienService.getAllCDs(); + Iterator iterator_cds = cds.iterator(); + while(iterator_cds.hasNext()) { + AudioCDEntity cd = iterator_cds.next(); + Element cdNode = document.createElement("audioCD"); + //cdNode.setAttribute("id", cd.getId()); + //cdNode.setAttribute("album", cd.getAlbum()); + //cdNode.setAttribute("artist", cd.getArtist()); + medien.appendChild(cdNode); + medien.appendChild(document.createTextNode("\n")); + } + + Collection films = medienService.getAllDVDs(); + Iterator iterator_films = films.iterator(); + while(iterator_films.hasNext()) { + FilmEntity film = iterator_films.next(); + Element filmNode = document.createElement("film"); + //filmNode.setAttribute("id", film.getId()); + //filmNode.setAttribute("title", film.getTitle()); + medien.appendChild(filmNode); + medien.appendChild(document.createTextNode("\n")); + } + + Collection boxsets = medienService.getAllBoxSets(); + Iterator iterator_boxsets = boxsets.iterator(); + while(iterator_boxsets.hasNext()) { + BoxSetEntity boxSet = iterator_boxsets.next(); + Element boxNode = document.createElement("boxSet"); + boxNode.setAttribute("id", boxSet.getId().toString()); + boxNode.setAttribute("title", boxSet.getTitle()); + films = boxSet.getFilms(); + iterator_films = films.iterator(); + if (iterator_films.hasNext()) { + boxNode.appendChild(document.createTextNode("\n")); + } + while(iterator_films.hasNext()) { + FilmEntity film = iterator_films.next(); + Element filmNode = document.createElement("film"); + //filmNode.setAttribute("id", film.getId()); + boxNode.appendChild(filmNode); + boxNode.appendChild(document.createTextNode("\n")); + } + medien.appendChild(boxNode); + medien.appendChild(document.createTextNode("\n")); + } + return medien; + } + + private MedienService getMedienService() + { + return null; + } +} diff --git a/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportTradingCards.java b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportTradingCards.java new file mode 100644 index 0000000..f17e424 --- /dev/null +++ b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportTradingCards.java @@ -0,0 +1,175 @@ +package com.peetz.kontor.data; + +import java.util.Collection; +import java.util.Iterator; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import com.peetz.tradingcards.entity.BaseSetEntity; +import com.peetz.tradingcards.entity.InsertEntity; +import com.peetz.tradingcards.entity.ManufacturerEntity; +import com.peetz.tradingcards.entity.ParallelSetEntity; +import com.peetz.tradingcards.entity.PositionEntity; +import com.peetz.tradingcards.entity.SportCardEntity; +import com.peetz.tradingcards.entity.SportEntity; +import com.peetz.tradingcards.entity.TeamEntity; +import com.peetz.tradingcards.service.SportService; +import com.peetz.tradingcards.service.TradingcardService; + +public class ExportTradingCards +{ + public Element backupTradingCards(Document document) + { + Element tradingcards = document.createElement("tradingcards"); + tradingcards.appendChild(document.createTextNode("\n")); + + SportService sportService = getSportService(); + backupSports(sportService, document, tradingcards); + + backupCards(document, tradingcards); + + return tradingcards; + } + + private void backupSports(SportService sportService, Document document, Element tradingcards) + { + Collection sports = sportService.getAllSports(); + Iterator iterator_sports = sports.iterator(); + while(iterator_sports.hasNext()) { + SportEntity sport = iterator_sports.next(); + Element sportNode = document.createElement("sport"); + sportNode.setAttribute("id", sport.getId().toString()); + sportNode.setAttribute("name", sport.getName()); + + Collection teams = sportService.getTeams(sport); + backupTeams(teams, document, sportNode); + + Collection positions = sportService.getPositions(sport); + backupPositions(positions, document, sportNode); + + tradingcards.appendChild(sportNode); + tradingcards.appendChild(document.createTextNode("\n")); + } + } + + private void backupTeams(Collection teams, Document document, Element sportNode) + { + Iterator iterator_teams = teams.iterator(); + while(iterator_teams.hasNext()) { + TeamEntity team = iterator_teams.next(); + Element teamNode = document.createElement("team"); + teamNode.setAttribute("id", team.getId().toString()); + teamNode.setAttribute("name", team.getName()); + //TODO what happens with null attributes? + //teamNode.setAttribute("short", teamView.getShortname()); + sportNode.appendChild(teamNode); + } + } + + private void backupPositions(Collection positions, Document document, Element sportNode) + { + Iterator iterator_positions = positions.iterator(); + while(iterator_positions.hasNext()) { + PositionEntity position = iterator_positions.next(); + Element positionNode = document.createElement("position"); + positionNode.setAttribute("id", position.getId().toString()); + positionNode.setAttribute("name", position.getName()); + positionNode.setAttribute("short", position.getShortName()); + sportNode.appendChild(positionNode); + } + sportNode.appendChild(document.createTextNode("\n")); + } + + private void backupCards(Document document, Element tradingcards) + { + Collection manufacturers = getTradingcardService().getAllManufacturers(); + Iterator iterator_manufacturers = manufacturers.iterator(); + while(iterator_manufacturers.hasNext()) { + ManufacturerEntity manufacturer = iterator_manufacturers.next(); + Element manufacturerNode = document.createElement("manufacturer"); + manufacturerNode.setAttribute("id", manufacturer.getId().toString()); + manufacturerNode.setAttribute("name", manufacturer.getName()); + Collection baseSets = getTradingcardService().getBaseSetsByManufacturer(manufacturer); + backupBaseSets(baseSets, document, manufacturerNode); + Collection parallelSets = getTradingcardService().getParallelSetsByManufacturer(manufacturer); + backupParallelSets(parallelSets, document, manufacturerNode); + Collection inserts = getTradingcardService().getInsertsByManufacturer(manufacturer); + backupInserts(inserts, document, manufacturerNode); + manufacturerNode.appendChild(document.createTextNode("\n")); + tradingcards.appendChild(manufacturerNode); + tradingcards.appendChild(document.createTextNode("\n")); + } + + Collection sportCards = getTradingcardService().getAllSportCards(); + Iterator iterator_sportCards = sportCards.iterator(); + while(iterator_sportCards.hasNext()) { + SportCardEntity sportCard = iterator_sportCards.next(); + Element cardNode = document.createElement("sportCard"); + cardNode.setAttribute("id", sportCard.getId().toString()); + cardNode.setAttribute("player", sportCard.getPlayer().getId().toString()); + cardNode.setAttribute("baseSet", sportCard.getBaseSet().getId().toString()); + if (sportCard.getParallelSet().getId() != null) + { + cardNode.setAttribute("parallelSet", sportCard.getParallelSet().getId().toString()); + } + if (sportCard.getInsert().getId() != null) + { + cardNode.setAttribute("insert", sportCard.getInsert().getId().toString()); + } + tradingcards.appendChild(cardNode); + tradingcards.appendChild(document.createTextNode("\n")); + } + } + + private void backupBaseSets(Collection baseSets, Document document, Element manufacturerNode) + { + Iterator iterator_baseSets= baseSets.iterator(); + while(iterator_baseSets.hasNext()) { + BaseSetEntity baseSet = iterator_baseSets.next(); + manufacturerNode.appendChild(document.createTextNode("\n")); + Element baseSetNode = document.createElement("baseSet"); + baseSetNode.setAttribute("id", baseSet.getId().toString()); + baseSetNode.setAttribute("name", baseSet.getName()); + manufacturerNode.appendChild(baseSetNode); + } + } + + private void backupParallelSets(Collection parallelSets, Document document, Element manufacturerNode) + { + Iterator iterator_parallelSets = parallelSets.iterator(); + while(iterator_parallelSets.hasNext()) { + ParallelSetEntity parallelSet = iterator_parallelSets.next(); + manufacturerNode.appendChild(document.createTextNode("\n")); + Element parallelSetNode = document.createElement("parallelSet"); + parallelSetNode.setAttribute("id", parallelSet.getId().toString()); + parallelSetNode.setAttribute("name", parallelSet.getName()); + parallelSetNode.setAttribute("baseSet", parallelSet.getBaseSet().getId().toString()); + manufacturerNode.appendChild(parallelSetNode); + } + } + + private void backupInserts(Collection inserts, Document document, Element manufacturerNode) + { + Iterator iterator_inserts = inserts.iterator(); + while(iterator_inserts.hasNext()) { + InsertEntity insert = iterator_inserts.next(); + manufacturerNode.appendChild(document.createTextNode("\n")); + Element insertNode = document.createElement("insert"); + insertNode.setAttribute("id", insert.getId().toString()); + insertNode.setAttribute("name", insert.getName()); + insertNode.setAttribute("baseSet", insert.getBaseSet().getId().toString()); + manufacturerNode.appendChild(insertNode); + } + } + + private SportService getSportService() + { + return null; + } + + private TradingcardService getTradingcardService() + { + return null; + } +} diff --git a/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/FileExport.java b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/FileExport.java new file mode 100644 index 0000000..fb1848f --- /dev/null +++ b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/FileExport.java @@ -0,0 +1,64 @@ +package com.peetz.kontor.data; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.FactoryConfigurationError; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.TransformerFactoryConfigurationError; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +public class FileExport +{ + public void exportFile(java.io.PrintWriter out) + { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.newDocument(); + ExportComics exportComics = new ExportComics(); + ExportMedien exportMedien = new ExportMedien(); + ExportLibrary exportLibrary = new ExportLibrary(); + ExportTradingCards exportTradingCards = new ExportTradingCards(); + + Element root = document.createElement("kontor"); + document.appendChild(root); + root.appendChild(document.createTextNode("\n")); + root.appendChild(exportComics.backupComics(document)); + root.appendChild(document.createTextNode("\n")); + root.appendChild(exportMedien.backupMedien(document)); + root.appendChild(document.createTextNode("\n")); + root.appendChild(exportLibrary.backupLibrary(document)); + root.appendChild(document.createTextNode("\n")); + root.appendChild(exportTradingCards.backupTradingCards(document)); + root.appendChild(document.createTextNode("\n")); + + TransformerFactory tFactory = TransformerFactory.newInstance(); + Transformer transformer = tFactory.newTransformer(); + + DOMSource source = new DOMSource(document); + StreamResult result = new StreamResult(out); + transformer.transform(source, result); + } catch (DOMException e) { + System.out.println(e.getMessage()); + } catch (TransformerConfigurationException e) { + System.out.println(e.getMessage()); + } catch (FactoryConfigurationError e) { + System.out.println(e.getMessage()); + } catch (ParserConfigurationException e) { + System.out.println(e.getMessage()); + } catch (TransformerFactoryConfigurationError e) { + System.out.println(e.getMessage()); + } catch (TransformerException e) { + System.out.println(e.getMessage()); + } + } +} diff --git a/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/FileImport.java b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/FileImport.java new file mode 100644 index 0000000..7b03dee --- /dev/null +++ b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/FileImport.java @@ -0,0 +1,72 @@ +package com.peetz.kontor.data; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.FactoryConfigurationError; +import javax.xml.parsers.ParserConfigurationException; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +public class FileImport +{ + public void importFile(File file) + { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(file); + + Node xmlNode = document.getFirstChild(); + if (!xmlNode.getNodeName().equals("kontor")) return; + NodeList nodeList = xmlNode.getChildNodes(); + for (int i=0; i songs = new ArrayList(); + + System.out.println("AudioCD: " + id + ", " + album + "/" + artist); + AudioCDEntity audioCD = getMedienService().addCD(album); + audioCD.setArtist(artist); + audioCD.setCategory(category); + audioCD.setReleaseYear(year); + audioCD.setWantList(wantList); + audioCD.getSongs().addAll(songs); + getMedienService().saveCD(audioCD); + } + + public void parseFilm(Node node) + { + NamedNodeMap attr = node.getAttributes(); + Node idNode = attr.getNamedItem("id"); + String id = idNode.getNodeValue(); + + Node titleNode = attr.getNamedItem("title"); + String title = titleNode.getNodeValue(); + + System.out.println("DVD: " + id + ", " + title); + MedienService service = getMedienService(); + + service.addDVD(title); + } + + public void parseBoxSet(Node node) + { + NamedNodeMap attr = node.getAttributes(); + Node idNode = attr.getNamedItem("id"); + String id = idNode.getNodeValue(); + + Node titleNode = attr.getNamedItem("title"); + String title = titleNode.getNodeValue(); + + System.out.println("BoxSet: " + id + ", " + title); + MedienService service = getMedienService(); + + service.addBoxSet(title); + } + + private MedienService getMedienService() + { + return null; + } +} diff --git a/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ImportTradingCards.java b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ImportTradingCards.java new file mode 100644 index 0000000..79c4e23 --- /dev/null +++ b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ImportTradingCards.java @@ -0,0 +1,52 @@ +package com.peetz.kontor.data; + +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import com.peetz.tradingcards.service.SportService; +import com.peetz.tradingcards.service.TradingcardService; + +public class ImportTradingCards +{ + public void parseNode(Node xmlNode) + { + NodeList childNodes = xmlNode.getChildNodes(); + for (int i=0; i + + + + org.eclipse.persistence.jpa.PersistenceProvider + jdbc/kontor + + + com.peetz.kontor.entity.KontorUserEntity + + com.peetz.comics.entity.ArtistEntity + com.peetz.comics.entity.ComicEntity + com.peetz.comics.entity.IssueEntity + com.peetz.comics.entity.PublisherEntity + com.peetz.comics.entity.StoryArcEntity + com.peetz.comics.entity.VolumeEntity + + com.peetz.library.entity.ArticleEntity + com.peetz.library.entity.BookEntity + com.peetz.library.entity.BookshelfEntity + com.peetz.library.entity.FileEntity + com.peetz.library.entity.MagazineEntity + com.peetz.library.entity.ShelfObjectEntity + com.peetz.library.entity.ShelfboardEntity + + com.peetz.medien.entity.AudioCDEntity + com.peetz.medien.entity.BoxSetEntity + com.peetz.medien.entity.FilmEntity + + com.peetz.tradingcards.entity.BaseSetEntity + com.peetz.tradingcards.entity.InsertEntity + com.peetz.tradingcards.entity.ManufacturerEntity + com.peetz.tradingcards.entity.ParallelSetEntity + com.peetz.tradingcards.entity.PlayerEntity + com.peetz.tradingcards.entity.PositionEntity + com.peetz.tradingcards.entity.SportCardEntity + com.peetz.tradingcards.entity.SportEntity + com.peetz.tradingcards.entity.TeamEntity + + + + + + diff --git a/java-ee/KontorWeb/src/main/webapp/WEB-INF/faces-config.xml b/java-ee/KontorWeb/src/main/webapp/WEB-INF/faces-config.xml new file mode 100644 index 0000000..77df4e5 --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/WEB-INF/faces-config.xml @@ -0,0 +1,77 @@ + + + + + kontor + /index.xhtml + + + comics + /comics.xhtml + + + library + /library.xhtml + + + medien + /medien.xhtml + + + tradingcards + /tradingcards.xhtml + + + sport + /sport.xhtml + + + sportAdd + /sport/sportAdd.xhtml + + + + /sport/sportAdd.xhtml + + addSport + /sport/sportDetails.xhtml + + + saveSport + /sport/sportDetails.xhtml + + + + now + java.util.Date + request + + + comicView + com.peetz.comics.view.ComicView + request + + + libraryView + com.peetz.library.view.LibraryView + request + + + medienView + com.peetz.medien.view.MedienView + request + + + tradingCardsView + com.peetz.tradingcards.view.TradingCardsView + request + + + sportView + com.peetz.tradingcards.view.SportView + request + + diff --git a/java-ee/KontorWeb/src/main/webapp/WEB-INF/glassfish-web.xml b/java-ee/KontorWeb/src/main/webapp/WEB-INF/glassfish-web.xml new file mode 100644 index 0000000..c95df12 --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/WEB-INF/glassfish-web.xml @@ -0,0 +1,7 @@ + + + + /kontor + + + diff --git a/java-ee/KontorWeb/src/main/webapp/WEB-INF/web.xml b/java-ee/KontorWeb/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..112c02f --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,22 @@ + + + + javax.faces.PROJECT_STAGE + Development + + + Faces Servlet + javax.faces.webapp.FacesServlet + 1 + + + Faces Servlet + /faces/* + + + 30 + + + faces/index.xhtml + + diff --git a/java-ee/KontorWeb/src/main/webapp/comics.xhtml b/java-ee/KontorWeb/src/main/webapp/comics.xhtml new file mode 100644 index 0000000..15d340f --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/comics.xhtml @@ -0,0 +1,39 @@ + + + + + + + + Kontor Application + + +
Comics Application
+
+
+ Kontor

+ Comics

+ Library

+ Medien

+ Trading Cards

+
+
+ +
+ + + + + + + + +
+
+
+
Ingenieurbüro Thomas Peetz
+ +
+ diff --git a/java-ee/KontorWeb/src/main/webapp/css/store.css b/java-ee/KontorWeb/src/main/webapp/css/store.css new file mode 100755 index 0000000..de05f30 --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/css/store.css @@ -0,0 +1,145 @@ +.spring { + border: thin solid black; + + font-size:10px; + font-family:Arial; + font-weight:normal; + background-color: #ABE7FA; + + } +.myButton { + -moz-box-shadow: 0px 10px 14px -7px #276873; + -webkit-box-shadow: 0px 10px 14px -7px #276873; + box-shadow: 0px 10px 14px -7px #276873; + background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #599bb3), color-stop(1, #408c99)); + background:-moz-linear-gradient(top, #599bb3 5%, #408c99 100%); + background:-webkit-linear-gradient(top, #599bb3 5%, #408c99 100%); + background:-o-linear-gradient(top, #599bb3 5%, #408c99 100%); + background:-ms-linear-gradient(top, #599bb3 5%, #408c99 100%); + background:linear-gradient(to bottom, #599bb3 5%, #408c99 100%); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#599bb3', endColorstr='#408c99',GradientType=0); + background-color:#599bb3; + -moz-border-radius:8px; + -webkit-border-radius:8px; + border-radius:8px; + display:inline-block; + cursor:pointer; + color:#ffffff; + font-family:Arial; + font-size:12px; + font-weight:bold; + padding:7px 15px; + text-decoration:none; + text-shadow:0px 1px 0px #3d768a; +} +.myButton:hover { + background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #408c99), color-stop(1, #599bb3)); + background:-moz-linear-gradient(top, #408c99 5%, #599bb3 100%); + background:-webkit-linear-gradient(top, #408c99 5%, #599bb3 100%); + background:-o-linear-gradient(top, #408c99 5%, #599bb3 100%); + background:-ms-linear-gradient(top, #408c99 5%, #599bb3 100%); + background:linear-gradient(to bottom, #408c99 5%, #599bb3 100%); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#408c99', endColorstr='#599bb3',GradientType=0); + background-color:#408c99; +} +.myButton:active { + position:relative; + top:1px; +} + + + .tablestyle { + margin:0px;padding:0px; + width:50%; + box-shadow: 10px 10px 5px #888888; + border:1px solid #000000; + + -moz-border-radius-bottomleft:0px; + -webkit-border-bottom-left-radius:0px; + border-bottom-left-radius:0px; + + -moz-border-radius-bottomright:0px; + -webkit-border-bottom-right-radius:0px; + border-bottom-right-radius:0px; + + -moz-border-radius-topright:0px; + -webkit-border-top-right-radius:0px; + border-top-right-radius:0px; + + -moz-border-radius-topleft:0px; + -webkit-border-top-left-radius:0px; + border-top-left-radius:0px; +}.tablestyle table{ + border-collapse: collapse; + border-spacing: 0; + width:100%; + height:100%; + margin:0px;padding:0px; +}.tablestyle tr:last-child td:last-child { + -moz-border-radius-bottomright:0px; + -webkit-border-bottom-right-radius:0px; + border-bottom-right-radius:0px; +} +.tablestyle table tr:first-child td:first-child { + -moz-border-radius-topleft:0px; + -webkit-border-top-left-radius:0px; + border-top-left-radius:0px; +} +.tablestyle table tr:first-child td:last-child { + -moz-border-radius-topright:0px; + -webkit-border-top-right-radius:0px; + border-top-right-radius:0px; +}.tablestyle tr:last-child td:first-child{ + -moz-border-radius-bottomleft:0px; + -webkit-border-bottom-left-radius:0px; + border-bottom-left-radius:0px; +}.tablestyle tr:hover td{ + +} +.tablestyle tr:nth-child(odd){ background-color:#aad4ff; } +.tablestyle tr:nth-child(even) { background-color:#ffffff; }.tablestyle td{ + vertical-align:middle; + + + border:1px solid #000000; + border-width:0px 1px 1px 0px; + text-align:left; + padding:7px; + font-size:10px; + font-family:Arial; + font-weight:normal; + color:#000000; +}.tablestyle tr:last-child td{ + border-width:0px 1px 0px 0px; +}.tablestyle tr td:last-child{ + border-width:0px 0px 1px 0px; +}.tablestyle tr:last-child td:last-child{ + border-width:0px 0px 0px 0px; +} +.tablestyle tr:first-child td{ + background:-o-linear-gradient(bottom, #005fbf 5%, #003f7f 100%); background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #005fbf), color-stop(1, #003f7f) ); + background:-moz-linear-gradient( center top, #005fbf 5%, #003f7f 100% ); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#005fbf", endColorstr="#003f7f"); background: -o-linear-gradient(top,#005fbf,003f7f); + + background-color:#005fbf; + border:0px solid #000000; + text-align:center; + border-width:0px 0px 1px 1px; + font-size:14px; + font-family:Arial; + font-weight:bold; + color:#ffffff; +} +.tablestyle tr:first-child:hover td{ + background:-o-linear-gradient(bottom, #005fbf 5%, #003f7f 100%); background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #005fbf), color-stop(1, #003f7f) ); + background:-moz-linear-gradient( center top, #005fbf 5%, #003f7f 100% ); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#005fbf", endColorstr="#003f7f"); background: -o-linear-gradient(top,#005fbf,003f7f); + + background-color:#005fbf; +} +.tablestyle tr:first-child td:first-child{ + border-width:0px 0px 1px 0px; +} +.tablestyle tr:first-child td:last-child{ + border-width:0px 0px 1px 1px; +} diff --git a/java-ee/KontorWeb/src/main/webapp/index.xhtml b/java-ee/KontorWeb/src/main/webapp/index.xhtml new file mode 100755 index 0000000..870d904 --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/index.xhtml @@ -0,0 +1,48 @@ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + +
+ + + + Key + + + + Value + + + +
+
+ diff --git a/java-ee/KontorWeb/src/main/webapp/kontorTemplate.xhtml b/java-ee/KontorWeb/src/main/webapp/kontorTemplate.xhtml new file mode 100644 index 0000000..90a8e65 --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/kontorTemplate.xhtml @@ -0,0 +1,44 @@ + + + + + + + + + Kontor Application + + + + +
+ Top +
+
+
+ + Kontor

+ Comics

+ Library

+ Medien + Trading Cards

+
+
+
+ +
+ Content +
+
+
+
+ Ingenieurbüro Thomas Peetz +
+ +
+ + diff --git a/java-ee/KontorWeb/src/main/webapp/library.xhtml b/java-ee/KontorWeb/src/main/webapp/library.xhtml new file mode 100644 index 0000000..ffeac85 --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/library.xhtml @@ -0,0 +1,39 @@ + + + + + + + + Kontor Application + + +
Library Application
+
+
+ Kontor

+ Comics

+ Library

+ Medien

+ Trading Cards

+
+
+ +
+ + + + + + + + +
+
+
+
Ingenieurbüro Thomas Peetz
+ +
+ diff --git a/java-ee/KontorWeb/src/main/webapp/medien.xhtml b/java-ee/KontorWeb/src/main/webapp/medien.xhtml new file mode 100644 index 0000000..b77bd49 --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/medien.xhtml @@ -0,0 +1,39 @@ + + + + + + + + Kontor Application + + +
Medien Application
+
+
+ Kontor

+ Comics

+ Library

+ Medien

+ Trading Cards

+
+
+ +
+ + + + + + + + +
+
+
+
Ingenieurbüro Thomas Peetz
+ +
+ diff --git a/java-ee/KontorWeb/src/main/webapp/resources/css/cssLayout.css b/java-ee/KontorWeb/src/main/webapp/resources/css/cssLayout.css new file mode 100644 index 0000000..b2f98ff --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/resources/css/cssLayout.css @@ -0,0 +1,71 @@ + +#top { + position: relative; + background-color: lightgrey; + text-align: center; + width: 100%; + height: 30px; + color: white; + padding: 5px; + //margin: 0px 0px 10px 0px; +} + +#bottom { + position: relative; + background-color: lightgrey; + width: 100%; + height: 30px; + padding: 5px; + //margin: 10px 0px 0px 0px; +} + +#left { + float: left; + background-color: tan; + padding: 5px; + width: 150px; + height: 50%; +} + +#right { + float: right; + background-color: tan; + //padding: 5px; + width: 150px; + height: 50%; +} + +.center_content { + position: relative; + background-color: wheat; + padding: 5px; + height: 50%; +} + +.left_content { + background-color: tan; + padding: 5px; + margin-left: 170px; + height: 50%; +} + +.right_content { + background-color: wheat; + padding: 5px; + //margin: 0px 170px 0px 170px; + height: 50%; +} + +#top a:link, #top a:visited { + color: white; + font-weight : bold; + text-decoration: none; +} + +#top a:link:hover, #top a:visited:hover { + color: black; + font-weight : bold; + text-decoration : underline; +} + + diff --git a/java-ee/KontorWeb/src/main/webapp/resources/css/default.css b/java-ee/KontorWeb/src/main/webapp/resources/css/default.css new file mode 100644 index 0000000..6cbc3d1 --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/resources/css/default.css @@ -0,0 +1,29 @@ +body { + background-color: #ffffff; + font-size: 12px; + font-family: Verdana, "Verdana CE", Arial, "Arial CE", "Lucida Grande CE", lucida, "Helvetica CE", sans-serif; + color: #000000; + margin: 10px; +} + +h1 { + font-family: Arial, "Arial CE", "Lucida Grande CE", lucida, "Helvetica CE", sans-serif; + border-bottom: 1px solid #AFAFAF; + font-size: 16px; + font-weight: bold; + margin: 0px; + padding: 0px; + color: #D20005; +} + +a:link, a:visited { + color: #045491; + font-weight : bold; + text-decoration: none; +} + +a:link:hover, a:visited:hover { + color: #045491; + font-weight : bold; + text-decoration : underline; +} diff --git a/java-ee/KontorWeb/src/main/webapp/seite.html b/java-ee/KontorWeb/src/main/webapp/seite.html new file mode 100644 index 0000000..8478fea --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/seite.html @@ -0,0 +1,17 @@ + + + + Testseite + + +
+
Testseite
+
+ +
Hauptseite
+
Details
+
+ +
+ + \ No newline at end of file diff --git a/java-ee/KontorWeb/src/main/webapp/sport.xhtml b/java-ee/KontorWeb/src/main/webapp/sport.xhtml new file mode 100644 index 0000000..840a202 --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/sport.xhtml @@ -0,0 +1,40 @@ + + + + + + + + Kontor Application + + +
Trading Cards Application
+
+
+ Kontor

+ Comics

+ Library

+ Medien

+ Trading Cards

+ Sport

+
+
+ +
+ + + + + + + + +
+
+
+
Ingenieurbüro Thomas Peetz
+ +
+ diff --git a/java-ee/KontorWeb/src/main/webapp/sport/sportAdd.xhtml b/java-ee/KontorWeb/src/main/webapp/sport/sportAdd.xhtml new file mode 100644 index 0000000..ee5ee2d --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/sport/sportAdd.xhtml @@ -0,0 +1,39 @@ + + + + + + + + Kontor Application + + +
Trading Cards Application
+
+
+ Kontor

+ Comics

+ Library

+ Medien

+ Trading Cards

+ Sport

+
+
+ +
+ + + + + + + +
+
+
+
Ingenieurbüro Thomas Peetz
+ +
+ diff --git a/java-ee/KontorWeb/src/main/webapp/sport/sportDetails.xhtml b/java-ee/KontorWeb/src/main/webapp/sport/sportDetails.xhtml new file mode 100644 index 0000000..cc6be43 --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/sport/sportDetails.xhtml @@ -0,0 +1,39 @@ + + + + + + + + Kontor Application + + +
Trading Cards Application
+
+
+ Kontor

+ Comics

+ Library

+ Medien

+ Trading Cards

+ Sport

+
+
+ +
+ + + + + + + +
+
+
+
Ingenieurbüro Thomas Peetz
+ +
+ diff --git a/java-ee/KontorWeb/src/main/webapp/tradingcards.xhtml b/java-ee/KontorWeb/src/main/webapp/tradingcards.xhtml new file mode 100644 index 0000000..585697e --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/tradingcards.xhtml @@ -0,0 +1,40 @@ + + + + + + + + Kontor Application + + +
Trading Cards Application
+
+
+ Kontor

+ Comics

+ Library

+ Medien

+ Trading Cards

+ Sport

+
+
+ +
+ + + + + + + + +
+
+
+
Ingenieurbüro Thomas Peetz
+ +
+ diff --git a/java-ee/LibraryImpl/build.gradle b/java-ee/LibraryImpl/build.gradle new file mode 100644 index 0000000..fa86a7f --- /dev/null +++ b/java-ee/LibraryImpl/build.gradle @@ -0,0 +1,5 @@ +jar { + manifest { + attributes 'Implementation-Title': 'Library', 'Implementation-Version': version + } +} diff --git a/java-ee/LibraryImpl/config/checkstyle/checkstyle.xml b/java-ee/LibraryImpl/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..7c682c3 --- /dev/null +++ b/java-ee/LibraryImpl/config/checkstyle/checkstyle.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java-ee/LibraryImpl/config/checkstyle/checkstyle.xsl b/java-ee/LibraryImpl/config/checkstyle/checkstyle.xsl new file mode 100644 index 0000000..393a01b --- /dev/null +++ b/java-ee/LibraryImpl/config/checkstyle/checkstyle.xsl @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

CheckStyle Audit

Designed for use with CheckStyle and Ant.
+
+ + + +
+ + + +
+ + + + + +

+

+ +


+ + + + +
+ + + + +

Files

+ + + + + + + + + + + + + + +
NameErrors
+
+ + + + +

File

+ + + + + + + + + + + + + +
Error DescriptionLine
+ Back to top +
+ + + +

Summary

+ + + + + + + + + + + + +
FilesErrors
+
+ + + + a + b + + +
+ + diff --git a/java-ee/LibraryImpl/config/findbugs/findbugs.xml b/java-ee/LibraryImpl/config/findbugs/findbugs.xml new file mode 100644 index 0000000..34a6e01 --- /dev/null +++ b/java-ee/LibraryImpl/config/findbugs/findbugs.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ArticleDao.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ArticleDao.java new file mode 100644 index 0000000..a62a119 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ArticleDao.java @@ -0,0 +1,25 @@ +package com.peetz.library.dal; + +import java.util.List; + +import javax.ejb.Local; + +import com.peetz.library.entity.ArticleEntity; + +@Local +public interface ArticleDao { + public ArticleEntity getById(Long id); + + public List findByIds(List ids); + + public List findByTitle(String title); + + public List getRelatedArticles(ArticleEntity entity); + + public ArticleEntity assign(ArticleEntity origin, ArticleEntity reference); + + public ArticleEntity store(ArticleEntity entity); + + public void delete(ArticleEntity entity); + +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/BookDao.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/BookDao.java new file mode 100644 index 0000000..56f2fea --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/BookDao.java @@ -0,0 +1,15 @@ +package com.peetz.library.dal; + +import java.util.Collection; + +import javax.ejb.Local; + +import com.peetz.library.entity.BookEntity; + +@Local +public interface BookDao { + public BookEntity findById(Long id); + public Collection findByIds(Collection ids); + public BookEntity store(BookEntity entity); + public void delete(BookEntity entity); +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/BookshelfDao.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/BookshelfDao.java new file mode 100644 index 0000000..ddf5b10 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/BookshelfDao.java @@ -0,0 +1,22 @@ +package com.peetz.library.dal; + +import java.util.List; + +import javax.ejb.Local; + +import com.peetz.library.entity.BookshelfEntity; + + +@Local +public interface BookshelfDao { + public BookshelfEntity getById(Long id); + + public List findByIds(List ids); + + public List findByTitle(String title); + + public BookshelfEntity store(BookshelfEntity entity); + + public void delete(BookshelfEntity entity); + +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/FileDao.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/FileDao.java new file mode 100644 index 0000000..b9e2e31 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/FileDao.java @@ -0,0 +1,15 @@ +package com.peetz.library.dal; + +import java.util.Collection; + +import javax.ejb.Local; + +import com.peetz.library.entity.FileEntity; + +@Local +public interface FileDao { + public FileEntity findById(Long id); + public Collection findByIds(Collection ids); + public FileEntity store(FileEntity entity); + public void delete(FileEntity entity); +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/MagazineDao.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/MagazineDao.java new file mode 100644 index 0000000..2df143e --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/MagazineDao.java @@ -0,0 +1,15 @@ +package com.peetz.library.dal; + +import java.util.Collection; + +import javax.ejb.Local; + +import com.peetz.library.entity.MagazineEntity; + +@Local +public interface MagazineDao { + public MagazineEntity findById(Long id); + public Collection findByIds(Collection ids); + public MagazineEntity store(MagazineEntity entity); + public void delete(MagazineEntity entity); +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ShelfObjectDao.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ShelfObjectDao.java new file mode 100644 index 0000000..2028863 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ShelfObjectDao.java @@ -0,0 +1,15 @@ +package com.peetz.library.dal; + +import java.util.Collection; + +import javax.ejb.Local; + +import com.peetz.library.entity.ShelfObjectEntity; + +@Local +public interface ShelfObjectDao { + public ShelfObjectEntity getById(Long id); + public Collection getByIds(Collection ids); + public ShelfObjectEntity store(ShelfObjectEntity entity); + public void delete(ShelfObjectEntity entity); +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ShelfboardDao.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ShelfboardDao.java new file mode 100644 index 0000000..ce326a7 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ShelfboardDao.java @@ -0,0 +1,15 @@ +package com.peetz.library.dal; + +import java.util.Collection; + +import javax.ejb.Local; + +import com.peetz.library.entity.ShelfboardEntity; + +@Local +public interface ShelfboardDao { + public ShelfboardEntity getById(Long id); + public Collection getByIds(Collection ids); + public ShelfboardEntity store(ShelfboardEntity entity); + public void delete(ShelfboardEntity entity); +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/package-info.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/package-info.java new file mode 100644 index 0000000..2c24b31 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author TPEETZ + * + */ +package com.peetz.library.dal; \ No newline at end of file diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ArticleEntity.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ArticleEntity.java new file mode 100644 index 0000000..0348e2b --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ArticleEntity.java @@ -0,0 +1,87 @@ +package com.peetz.library.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToMany; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Article.findAll", query="SELECT a from ArticleEntity as a") +}) + +@Entity +@Table(name="ARTICLE") +public class ArticleEntity { + private Long id; + + private String title; + + private Collection relatedArticles = new ArrayList(); + + private Collection originArticles = new ArrayList(); + + private FileEntity file; + + private MagazineEntity magazine; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @ManyToMany + public Collection getRelatedArticles() { + return relatedArticles; + } + + public void setRelatedArticles(Collection relatedArticles) { + this.relatedArticles = relatedArticles; + } + + @ManyToMany + public Collection getOriginArticles() { + return originArticles; + } + + public void setOriginArticles(Collection originArticles) { + this.originArticles = originArticles; + } + + @ManyToOne + public FileEntity getFile() { + return file; + } + + public void setFile(FileEntity file) { + this.file = file; + } + + @ManyToOne + public MagazineEntity getMagazine() { + return magazine; + } + + public void setMagazine(MagazineEntity magazine) { + this.magazine = magazine; + } +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/BookEntity.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/BookEntity.java new file mode 100644 index 0000000..1f86ba0 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/BookEntity.java @@ -0,0 +1,93 @@ +package com.peetz.library.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Book.findAll", query="SELECT a from BookEntity as a") +}) + +@Entity +@Table(name="BOOK") +public class BookEntity { + private Long id; + + private String title; + + private String author; + + private String publisher; + + private String isbn; + + private Long page; + + private String edition; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @Column + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + @Column + public String getPublisher() { + return publisher; + } + + public void setPublisher(String publisher) { + this.publisher = publisher; + } + + @Column + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + @Column + public Long getPage() { + return page; + } + + public void setPage(Long page) { + this.page = page; + } + + @Column + public String getEdition() { + return edition; + } + + public void setEdition(String edition) { + this.edition = edition; + } +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/BookshelfEntity.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/BookshelfEntity.java new file mode 100644 index 0000000..521a9bf --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/BookshelfEntity.java @@ -0,0 +1,53 @@ +package com.peetz.library.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Bookshelf.findAll", query="SELECT b from BookshelfEntity as b"), + @NamedQuery(name="Bookshelf.findById", query="SELECT b from BookshelfEntity as b WHERE b.id = :id"), + @NamedQuery(name="Bookshelf.findByTitle", query="SELECT b from BookshelfEntity as b WHERE b.title = :title") +}) + +@Entity +@Table(name="BOOKSHELF") +public class BookshelfEntity { + + private Long id; + + private String title; + + private Collection shelfBoards = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + @OneToMany(mappedBy="bookshelf", cascade=CascadeType.REMOVE) + public Collection getShelfBoards() { + return shelfBoards; + } + + public void setShelfBoards(Collection shelfBoards) { + this.shelfBoards = shelfBoards; + } +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/FileEntity.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/FileEntity.java new file mode 100644 index 0000000..6f31553 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/FileEntity.java @@ -0,0 +1,49 @@ +package com.peetz.library.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="FILE") +public class FileEntity { + private Long id; + + private String title; + + private Collection articles = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @OneToMany(mappedBy="file", cascade=CascadeType.REMOVE) + public Collection getArticles() { + return articles; + } + + public void setArticles(Collection articles) { + this.articles = articles; + } + +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/MagazineEntity.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/MagazineEntity.java new file mode 100644 index 0000000..6bed864 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/MagazineEntity.java @@ -0,0 +1,49 @@ +package com.peetz.library.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="MAGAZINE") +public class MagazineEntity { + private Long id; + + private String title; + + private Collection articles = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @OneToMany(mappedBy="magazine", cascade=CascadeType.REMOVE) + public Collection getArticles() { + return articles; + } + + public void setArticles(Collection articles) { + this.articles = articles; + } + +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ShelfObjectEntity.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ShelfObjectEntity.java new file mode 100644 index 0000000..98a5cd1 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ShelfObjectEntity.java @@ -0,0 +1,32 @@ +package com.peetz.library.entity; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name="SHELFOBJECT") +public class ShelfObjectEntity { + private Long id; + + private ShelfboardEntity shelfboard; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @ManyToOne + public ShelfboardEntity getShelfboard() { + return shelfboard; + } + + public void setShelfboard(ShelfboardEntity shelfboard) { + this.shelfboard = shelfboard; + } +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ShelfboardEntity.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ShelfboardEntity.java new file mode 100644 index 0000000..d6e4edd --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ShelfboardEntity.java @@ -0,0 +1,57 @@ +package com.peetz.library.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="SHELFBOARD") +public class ShelfboardEntity { + private Long id; + + private String title; + + private BookshelfEntity bookshelf; + + private Collection objects = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + @ManyToOne + public BookshelfEntity getBookshelf() { + return bookshelf; + } + + public void setBookshelf(BookshelfEntity bookshelf) { + this.bookshelf = bookshelf; + } + + @OneToMany(mappedBy="shelfboard", cascade=CascadeType.REMOVE) + public Collection getObjects() { + return objects; + } + + public void setObjects(Collection objects) { + this.objects = objects; + } + +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/package-info.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/package-info.java new file mode 100644 index 0000000..94536f3 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author TPEETZ + * + */ +package com.peetz.library.entity; \ No newline at end of file diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/service/LibraryService.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/service/LibraryService.java new file mode 100644 index 0000000..7ac294f --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/service/LibraryService.java @@ -0,0 +1,28 @@ +package com.peetz.library.service; + +import java.util.Collection; + +import javax.ejb.Local; + +import com.peetz.library.entity.ArticleEntity; +import com.peetz.library.entity.BookEntity; +import com.peetz.library.entity.BookshelfEntity; + +@Local +public interface LibraryService { + + Collection getAllBooks(); + + Collection getAllBookshelfs(); + + Collection getAllArticles(); + + void addBookshelf(String title); + + void addArticle(String title); + + BookEntity addBook(String title); + + void saveBook(BookEntity book); + +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/service/LibraryServiceImpl.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/service/LibraryServiceImpl.java new file mode 100644 index 0000000..62baae8 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/service/LibraryServiceImpl.java @@ -0,0 +1,64 @@ +package com.peetz.library.service; + +import java.util.Collection; + +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; + +import com.peetz.library.entity.ArticleEntity; +import com.peetz.library.entity.BookEntity; +import com.peetz.library.entity.BookshelfEntity; +import java.util.ArrayList; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +@Stateless(name="LibraryService") +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class LibraryServiceImpl implements LibraryService { + + @PersistenceContext(unitName = "kontor") + private EntityManager em; + + @SuppressWarnings("unchecked") + @Override + public Collection getAllBooks() { + Query query = em.createNamedQuery("Book.findAll"); + ArrayList bookList = new ArrayList(query.getResultList()); + return bookList; + } + + @SuppressWarnings("unchecked") + @Override + public Collection getAllBookshelfs() { + Query query = em.createNamedQuery("Bookshelf.findAll"); + ArrayList bookshelfList = new ArrayList(query.getResultList()); + return bookshelfList; + } + + @SuppressWarnings("unchecked") + @Override + public Collection getAllArticles() { + Query query = em.createNamedQuery("Article.findAll"); + ArrayList articleList = new ArrayList(query.getResultList()); + return articleList; + } + + @Override + public void addBookshelf(String title) { + } + + @Override + public void addArticle(String title) { + } + + @Override + public BookEntity addBook(String title) { + return null; + } + + @Override + public void saveBook(BookEntity book) { + } +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/service/package-info.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/service/package-info.java new file mode 100644 index 0000000..4a56b54 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/service/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author TPEETZ + * + */ +package com.peetz.library.service; \ No newline at end of file diff --git a/java-ee/LibraryWeb/build.gradle b/java-ee/LibraryWeb/build.gradle new file mode 100644 index 0000000..9a41aae --- /dev/null +++ b/java-ee/LibraryWeb/build.gradle @@ -0,0 +1,7 @@ +apply plugin: 'war' + +version = '0.0.1' + +dependencies { + compile project(':LibraryImpl') +} diff --git a/java-ee/LibraryWeb/src/main/java/com/peetz/library/view/LibraryView.java b/java-ee/LibraryWeb/src/main/java/com/peetz/library/view/LibraryView.java new file mode 100644 index 0000000..9a566a0 --- /dev/null +++ b/java-ee/LibraryWeb/src/main/java/com/peetz/library/view/LibraryView.java @@ -0,0 +1,42 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package com.peetz.library.view; + +import com.peetz.library.service.LibraryService; +import java.io.Serializable; +import java.util.logging.Logger; +import javax.ejb.EJB; +import javax.faces.bean.ManagedBean; +import javax.faces.bean.RequestScoped; + +/** + * + * @author tpeetz + */ +@ManagedBean(name="LibraryView") +@RequestScoped +public class LibraryView implements Serializable { + + private static final long serialVersionUID = -6251848426914654974L; + + private static final Logger LOG = Logger.getLogger(LibraryView.class.getName()); + + @EJB + private LibraryService libraryService; + + public LibraryView() { + LOG.info("LibraryView created"); + } + + public Integer getBookNumber() { + return libraryService.getAllBooks().size(); + } + + public Integer getArticleNumber() { + return libraryService.getAllArticles().size(); + } +} diff --git a/java-ee/LibraryWeb/src/main/webapp/index.jsp b/java-ee/LibraryWeb/src/main/webapp/index.jsp new file mode 100644 index 0000000..9e061a7 --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/index.jsp @@ -0,0 +1,33 @@ + + Library Application + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
Kontor +

Library Manager

+ Show the book list +
 
+

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/articleAdd.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/articleAdd.jsp new file mode 100644 index 0000000..63f9266 --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/articleAdd.jsp @@ -0,0 +1,67 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Title:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/articleEdit.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/articleEdit.jsp new file mode 100644 index 0000000..d3ae5e5 --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/articleEdit.jsp @@ -0,0 +1,69 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Titel:
+ <%-- hidden fields for id and userId --%> + + <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/articleList.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/articleList.jsp new file mode 100644 index 0000000..d8570ba --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/articleList.jsp @@ -0,0 +1,90 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Show article list
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + + + + <%-- set the header --%> + + + + + + <%-- check if article exists and display message or iterate over articles --%> + + + + + + + + + <%-- print out the article informations --%> + + <%-- print out the edit and delete link for each article --%> + + + + + + <%-- end interate --%> + + <%-- if articles cannot be found display a text --%> + + + + + + + +
Article Title  
No articles available
EditDelete
No articles found.
+
+ <%-- add and back to menu button --%> + Add a new article + +   + Back to menu + + +
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/boardAdd.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/boardAdd.jsp new file mode 100644 index 0000000..e1bc491 --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/boardAdd.jsp @@ -0,0 +1,68 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + + +
Shelf:
Title:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/boardEdit.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/boardEdit.jsp new file mode 100644 index 0000000..7221317 --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/boardEdit.jsp @@ -0,0 +1,67 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Title:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/bookAdd.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/bookAdd.jsp new file mode 100644 index 0000000..d3fb02b --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/bookAdd.jsp @@ -0,0 +1,67 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Title:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/bookEdit.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/bookEdit.jsp new file mode 100644 index 0000000..8d54239 --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/bookEdit.jsp @@ -0,0 +1,85 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + + + + + + + + + + + + + + + + + +
Titel:
Autor:
Verlag:
ISBN:
Seiten:
+ <%-- hidden fields for id and userId --%> + + <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/bookList.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/bookList.jsp new file mode 100644 index 0000000..9c1d40d --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/bookList.jsp @@ -0,0 +1,33 @@ + + Library Application + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
Kontor +

Library Manager

+ +
 
+

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/index.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/index.jsp new file mode 100644 index 0000000..e75087a --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/index.jsp @@ -0,0 +1,58 @@ +<%@ page language="java" contentType="text/html; charset=ISO-8859-1" + pageEncoding="ISO-8859-1"%> +<%@taglib uri="http://java.sun.com/jstl/core" prefix="c"%> +<%@taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html" %> + + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + +

Library Manager

+ Show the booklist +
+ Import CD List +
+ + + Import Books + +
+ Show the bookshelfs +
+ Show the articles +
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/shelfAdd.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/shelfAdd.jsp new file mode 100644 index 0000000..30b7e3b --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/shelfAdd.jsp @@ -0,0 +1,67 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Title:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/shelfEdit.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/shelfEdit.jsp new file mode 100644 index 0000000..a7c56af --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/shelfEdit.jsp @@ -0,0 +1,92 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Title:
+ <%-- hidden fields for id and userId --%> + + <%-- set the parameter for the dispatch action --%> + + +
+ + + + + + + + + + + + + + + + + + + +
Shelfboard  
No boards available
EditDelete
No boards available
Add board
+
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/shelfList.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/shelfList.jsp new file mode 100644 index 0000000..3313853 --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/shelfList.jsp @@ -0,0 +1,91 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Show shelf list
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + + + + <%-- set the header --%> + + + + + + + <%-- check if bookshelf exists and display message or iterate over bookshelfs --%> + + + + + + + + + <%-- print out the book informations --%> + + + <%-- print out the edit and delete link for each book --%> + + + + + + <%-- end interate --%> + + <%-- if books cannot be found display a text --%> + + + + + + + +
Bookshelf name# Shelfs  
No shelfs available
EditDelete
No shelfs found.
+
+ <%-- add and back to menu button --%> + Add a new book shelf +   + Back to menu + + +
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/MedienImpl/build.gradle b/java-ee/MedienImpl/build.gradle new file mode 100644 index 0000000..a08e29c --- /dev/null +++ b/java-ee/MedienImpl/build.gradle @@ -0,0 +1,5 @@ +jar { + manifest { + attributes 'Implementation-Title': 'Medien', 'Implementation-Version': version + } +} diff --git a/java-ee/MedienImpl/config/checkstyle/checkstyle.xml b/java-ee/MedienImpl/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..7c682c3 --- /dev/null +++ b/java-ee/MedienImpl/config/checkstyle/checkstyle.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java-ee/MedienImpl/config/checkstyle/checkstyle.xsl b/java-ee/MedienImpl/config/checkstyle/checkstyle.xsl new file mode 100644 index 0000000..393a01b --- /dev/null +++ b/java-ee/MedienImpl/config/checkstyle/checkstyle.xsl @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

CheckStyle Audit

Designed for use with CheckStyle and Ant.
+
+ + + +
+ + + +
+ + + + + +

+

+ +


+ + + + +
+ + + + +

Files

+ + + + + + + + + + + + + + +
NameErrors
+
+ + + + +

File

+ + + + + + + + + + + + + +
Error DescriptionLine
+ Back to top +
+ + + +

Summary

+ + + + + + + + + + + + +
FilesErrors
+
+ + + + a + b + + +
+ + diff --git a/java-ee/MedienImpl/config/findbugs/findbugs.xml b/java-ee/MedienImpl/config/findbugs/findbugs.xml new file mode 100644 index 0000000..34a6e01 --- /dev/null +++ b/java-ee/MedienImpl/config/findbugs/findbugs.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java-ee/MedienImpl/src/main/java/com/peetz/medien/dal/package-info.java b/java-ee/MedienImpl/src/main/java/com/peetz/medien/dal/package-info.java new file mode 100644 index 0000000..a144dbc --- /dev/null +++ b/java-ee/MedienImpl/src/main/java/com/peetz/medien/dal/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author TPEETZ + * + */ +package com.peetz.medien.dal; \ No newline at end of file diff --git a/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/AudioCDEntity.java b/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/AudioCDEntity.java new file mode 100644 index 0000000..63cadc8 --- /dev/null +++ b/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/AudioCDEntity.java @@ -0,0 +1,91 @@ +package com.peetz.medien.entity; + +import java.util.Collection; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="AudioCD.findAll", query="SELECT c from AudioCDEntity as c"), + @NamedQuery(name="AudioCD.findByAlbum", query="SELECT c from AudioCDEntity as c WHERE c.album = :album") +}) + +@Entity +@Table(name="AUDIOCD") +public class AudioCDEntity +{ + private Long id; + private String album; + private String artist; + private String category; + private String releaseYear; + private Boolean wantList; + private Collection songs; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getAlbum() { + return album; + } + + public void setAlbum(String album) { + this.album = album; + } + + @Column + public String getArtist() { + return artist; + } + + public void setArtist(String artist) { + this.artist = artist; + } + + @Column + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + @Column + public String getReleaseYear() { + return releaseYear; + } + + public void setReleaseYear(String releaseYear) { + this.releaseYear = releaseYear; + } + + @Column + public Boolean getWantList() { + return wantList; + } + + public void setWantList(Boolean wantList) { + this.wantList = wantList; + } + + @Column + public Collection getSongs() { + return songs; + } + + public void setSongs(Collection songs) { + this.songs = songs; + } +} diff --git a/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/BoxSetEntity.java b/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/BoxSetEntity.java new file mode 100644 index 0000000..ad3bba9 --- /dev/null +++ b/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/BoxSetEntity.java @@ -0,0 +1,43 @@ +package com.peetz.medien.entity; + +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="BOXSET") +public class BoxSetEntity +{ + private Long id; + private String title; + private Collection films; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + @OneToMany(mappedBy="boxSet", cascade=CascadeType.REMOVE) + public Collection getFilms() { + return films; + } + + public void setFilms(Collection films) { + this.films = films; + } + +} diff --git a/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/FilmEntity.java b/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/FilmEntity.java new file mode 100644 index 0000000..7be93e0 --- /dev/null +++ b/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/FilmEntity.java @@ -0,0 +1,50 @@ +package com.peetz.medien.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Film.findAll", query="SELECT f from FilmEntity as f"), + @NamedQuery(name="Film.findByTitle", query="SELECT f from FilmEntity as f WHERE f.title = :title") +}) + +@Entity +@Table(name="FILM") +public class FilmEntity +{ + private Long id; + private String title; + private BoxSetEntity boxSet; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @ManyToOne + public BoxSetEntity getBoxSet() { + return boxSet; + } + + public void setBoxSet(BoxSetEntity boxSet) { + this.boxSet = boxSet; + } +} diff --git a/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/package-info.java b/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/package-info.java new file mode 100644 index 0000000..803ef9c --- /dev/null +++ b/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author TPEETZ + * + */ +package com.peetz.medien.entity; \ No newline at end of file diff --git a/java-ee/MedienImpl/src/main/java/com/peetz/medien/service/MedienService.java b/java-ee/MedienImpl/src/main/java/com/peetz/medien/service/MedienService.java new file mode 100644 index 0000000..a1fc92a --- /dev/null +++ b/java-ee/MedienImpl/src/main/java/com/peetz/medien/service/MedienService.java @@ -0,0 +1,22 @@ +package com.peetz.medien.service; + +import java.util.Collection; + +import javax.ejb.Local; + +import com.peetz.medien.entity.AudioCDEntity; +import com.peetz.medien.entity.BoxSetEntity; +import com.peetz.medien.entity.FilmEntity; + +@Local +public interface MedienService +{ + public Collection getAllCDs(); + public AudioCDEntity addCD(String title); + public Collection getAllDVDs(); + public FilmEntity addDVD(String title); + public Collection getAllBoxSets(); + public BoxSetEntity addBoxSet(String title); + public BoxSetEntity assignBoxSet(BoxSetEntity boxSet, FilmEntity film); + public void saveCD(AudioCDEntity audioCD); +} diff --git a/java-ee/MedienImpl/src/main/java/com/peetz/medien/service/MedienServiceImpl.java b/java-ee/MedienImpl/src/main/java/com/peetz/medien/service/MedienServiceImpl.java new file mode 100644 index 0000000..9e08e58 --- /dev/null +++ b/java-ee/MedienImpl/src/main/java/com/peetz/medien/service/MedienServiceImpl.java @@ -0,0 +1,75 @@ +package com.peetz.medien.service; + +import java.util.Collection; + +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; + +import com.peetz.medien.entity.AudioCDEntity; +import com.peetz.medien.entity.BoxSetEntity; +import com.peetz.medien.entity.FilmEntity; +import java.util.ArrayList; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +@Stateless(name="MedienService") +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class MedienServiceImpl implements MedienService { + + @PersistenceContext(unitName = "kontor") + private EntityManager em; + + @SuppressWarnings("unchecked") + @Override + public Collection getAllCDs() { + Query query = em.createNamedQuery("AudioCD.findAll"); + ArrayList cdList = new ArrayList(query.getResultList()); + return cdList; + } + + @Override + public AudioCDEntity addCD(String title) { + // TODO Auto-generated method stub + return null; + } + + @SuppressWarnings("unchecked") + @Override + public Collection getAllDVDs() { + Query query = em.createNamedQuery("Film.findAll"); + ArrayList filmList = new ArrayList(query.getResultList()); + return filmList; + } + + @Override + public FilmEntity addDVD(String title) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Collection getAllBoxSets() { + // TODO Auto-generated method stub + return null; + } + + @Override + public BoxSetEntity addBoxSet(String title) { + // TODO Auto-generated method stub + return null; + } + + @Override + public BoxSetEntity assignBoxSet(BoxSetEntity boxSet, FilmEntity film) { + // TODO Auto-generated method stub + return null; + } + + @Override + public void saveCD(AudioCDEntity audioCD) { + // TODO Auto-generated method stub + + } +} diff --git a/java-ee/MedienImpl/src/main/java/com/peetz/medien/service/package-info.java b/java-ee/MedienImpl/src/main/java/com/peetz/medien/service/package-info.java new file mode 100644 index 0000000..ab20de0 --- /dev/null +++ b/java-ee/MedienImpl/src/main/java/com/peetz/medien/service/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author TPEETZ + * + */ +package com.peetz.medien.service; \ No newline at end of file diff --git a/java-ee/MedienWeb/build.gradle b/java-ee/MedienWeb/build.gradle new file mode 100644 index 0000000..73a53fa --- /dev/null +++ b/java-ee/MedienWeb/build.gradle @@ -0,0 +1,7 @@ +apply plugin: 'war' + +version = '0.0.1' + +dependencies { + compile project(':MedienImpl') +} diff --git a/java-ee/MedienWeb/src/main/java/com/peetz/medien/view/MedienView.java b/java-ee/MedienWeb/src/main/java/com/peetz/medien/view/MedienView.java new file mode 100644 index 0000000..ef14235 --- /dev/null +++ b/java-ee/MedienWeb/src/main/java/com/peetz/medien/view/MedienView.java @@ -0,0 +1,36 @@ +package com.peetz.medien.view; + +import com.peetz.medien.service.MedienService; +import java.io.Serializable; +import java.util.logging.Logger; +import javax.ejb.EJB; +import javax.faces.bean.ManagedBean; +import javax.faces.bean.RequestScoped; + +/** + * + * @author TPEETZ + */ +@ManagedBean(name="MedienView") +@RequestScoped +public class MedienView implements Serializable { + + private static final Logger LOG = Logger.getLogger(MedienView.class.getName()); + + @EJB + private MedienService medienService; + + private static final long serialVersionUID = -8261128991042235283L; + + public MedienView() { + LOG.info("MedienView created"); + } + + public Integer getCdNumber() { + return medienService.getAllCDs().size(); + } + + public Integer getDvdNumber() { + return medienService.getAllDVDs().size(); + } +} diff --git a/java-ee/MedienWeb/src/main/webapp/index.jsp b/java-ee/MedienWeb/src/main/webapp/index.jsp new file mode 100644 index 0000000..3e682f8 --- /dev/null +++ b/java-ee/MedienWeb/src/main/webapp/index.jsp @@ -0,0 +1,33 @@ + + Medien Application + + + + + + + + + + + + + + + + + + + + + + + +
Medien Manager
test +

Medien Manager

+ Show the media list +
 
+

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/java-ee/MedienWeb/src/main/webapp/jsp/cdAdd.jsp b/java-ee/MedienWeb/src/main/webapp/jsp/cdAdd.jsp new file mode 100644 index 0000000..f61357f --- /dev/null +++ b/java-ee/MedienWeb/src/main/webapp/jsp/cdAdd.jsp @@ -0,0 +1,75 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Medien Application + + + + + + + + + + + + + + + + + + + + + + + + +
Show CD list
+ <% out.println(com.peetz.medien.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + + + + + + + + + +
Category:
Album:
Artist:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/MedienWeb/src/main/webapp/jsp/cdEdit.jsp b/java-ee/MedienWeb/src/main/webapp/jsp/cdEdit.jsp new file mode 100644 index 0000000..f61357f --- /dev/null +++ b/java-ee/MedienWeb/src/main/webapp/jsp/cdEdit.jsp @@ -0,0 +1,75 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Medien Application + + + + + + + + + + + + + + + + + + + + + + + + +
Show CD list
+ <% out.println(com.peetz.medien.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + + + + + + + + + +
Category:
Album:
Artist:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/MedienWeb/src/main/webapp/jsp/cdList.jsp b/java-ee/MedienWeb/src/main/webapp/jsp/cdList.jsp new file mode 100644 index 0000000..ad936ab --- /dev/null +++ b/java-ee/MedienWeb/src/main/webapp/jsp/cdList.jsp @@ -0,0 +1,94 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Medien Application + + + + + + + + + + + + + + + + + + + + + + + + +
Show CD list
+ <% out.println(com.peetz.medien.navigation.MenuLinks.getInstance().toString()); %> + + + + <%-- set the header --%> + + + + + + + + <%-- check if book exists and display message or iterate over cds --%> + + + + + + + + + <%-- print out the book informations --%> + + + + <%-- print out the edit and delete link for each CD --%> + + + + + + <%-- end interate --%> + + <%-- if cds cannot be found display a text --%> + + + + + + + +
CategoryAlbum titleArtist  
No CDs available
EditDelete
No CDs found.
+
+ <%-- add and back to menu button --%> + Add a new CD + +   + Back to menu + + +
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/MedienWeb/src/main/webapp/jsp/dvdAdd.jsp b/java-ee/MedienWeb/src/main/webapp/jsp/dvdAdd.jsp new file mode 100644 index 0000000..e9d6617 --- /dev/null +++ b/java-ee/MedienWeb/src/main/webapp/jsp/dvdAdd.jsp @@ -0,0 +1,67 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Medien Application + + + + + + + + + + + + + + + + + + + + + + + + +
Show CD list
+ <% out.println(com.peetz.medien.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Film:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/MedienWeb/src/main/webapp/jsp/dvdEdit.jsp b/java-ee/MedienWeb/src/main/webapp/jsp/dvdEdit.jsp new file mode 100644 index 0000000..9044d68 --- /dev/null +++ b/java-ee/MedienWeb/src/main/webapp/jsp/dvdEdit.jsp @@ -0,0 +1,69 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Medien Application + + + + + + + + + + + + + + + + + + + + + + + + +
Show CD list
+ <% out.println(com.peetz.medien.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Film:
+ <%-- hidden fields for id and userId --%> + + <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/MedienWeb/src/main/webapp/jsp/dvdList.jsp b/java-ee/MedienWeb/src/main/webapp/jsp/dvdList.jsp new file mode 100644 index 0000000..6853fcd --- /dev/null +++ b/java-ee/MedienWeb/src/main/webapp/jsp/dvdList.jsp @@ -0,0 +1,90 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Medien Application + + + + + + + + + + + + + + + + + + + + + + + + +
Show DVD list
+ <% out.println(com.peetz.medien.navigation.MenuLinks.getInstance().toString()); %> + + + + <%-- set the header --%> + + + + + + <%-- check if book exists and display message or iterate over books --%> + + + + + + + + + <%-- print out the DVD informations --%> + + <%-- print out the edit and delete link for each DVD --%> + + + + + + <%-- end interate --%> + + <%-- if dvds cannot be found display a text --%> + + + + + + + +
DVD title  
No DVDs available
EditDelete
No DVDs found.
+
+ <%-- add and back to menu button --%> + Add a new DVD + +   + Back to menu + + +
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/MedienWeb/src/main/webapp/jsp/index.jsp b/java-ee/MedienWeb/src/main/webapp/jsp/index.jsp new file mode 100644 index 0000000..2b8b61f --- /dev/null +++ b/java-ee/MedienWeb/src/main/webapp/jsp/index.jsp @@ -0,0 +1,58 @@ +<%@ page language="java" contentType="text/html; charset=ISO-8859-1" + pageEncoding="ISO-8859-1"%> +<%@taglib uri="http://java.sun.com/jstl/core" prefix="c"%> +<%@taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html" %> + + + + + + + + + Medien Application + + + + + + + + + + + + + + + + + + + + + + + + +
Medien Manager (CD, DVD)
+ <% out.println(com.peetz.medien.navigation.MenuLinks.getInstance().toString()); %> + +

Medien Manager

+

CD Liste

+

Import CD List

+ + + Import CDs + +

DVD Liste

+

Import DVD List

+ + + Import DVDs + +
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/README.md b/java-ee/README.md new file mode 100644 index 0000000..91ce4c6 --- /dev/null +++ b/java-ee/README.md @@ -0,0 +1,2 @@ +# Kontor Java Enterprise Edition + diff --git a/java-ee/TradingCardsImpl/build.gradle b/java-ee/TradingCardsImpl/build.gradle new file mode 100644 index 0000000..6e6daf4 --- /dev/null +++ b/java-ee/TradingCardsImpl/build.gradle @@ -0,0 +1,5 @@ +jar { + manifest { + attributes 'Implementation-Title': 'TradingCards', 'Implementation-Version': version + } +} diff --git a/java-ee/TradingCardsImpl/config/checkstyle/checkstyle.xml b/java-ee/TradingCardsImpl/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..7c682c3 --- /dev/null +++ b/java-ee/TradingCardsImpl/config/checkstyle/checkstyle.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java-ee/TradingCardsImpl/config/checkstyle/checkstyle.xsl b/java-ee/TradingCardsImpl/config/checkstyle/checkstyle.xsl new file mode 100644 index 0000000..393a01b --- /dev/null +++ b/java-ee/TradingCardsImpl/config/checkstyle/checkstyle.xsl @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

CheckStyle Audit

Designed for use with CheckStyle and Ant.
+
+ + + +
+ + + +
+ + + + + +

+

+ +


+ + + + +
+ + + + +

Files

+ + + + + + + + + + + + + + +
NameErrors
+
+ + + + +

File

+ + + + + + + + + + + + + +
Error DescriptionLine
+ Back to top +
+ + + +

Summary

+ + + + + + + + + + + + +
FilesErrors
+
+ + + + a + b + + +
+ + diff --git a/java-ee/TradingCardsImpl/config/findbugs/findbugs.xml b/java-ee/TradingCardsImpl/config/findbugs/findbugs.xml new file mode 100644 index 0000000..34a6e01 --- /dev/null +++ b/java-ee/TradingCardsImpl/config/findbugs/findbugs.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/ManufacturerDao.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/ManufacturerDao.java new file mode 100644 index 0000000..e216bb2 --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/ManufacturerDao.java @@ -0,0 +1,31 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package com.peetz.tradingcards.dal; + +import com.peetz.tradingcards.entity.BaseSetEntity; +import com.peetz.tradingcards.entity.ManufacturerEntity; +import java.util.List; +import javax.ejb.Local; + +/** + * + * @author tpeetz + */ +@Local +public interface ManufacturerDao { + public ManufacturerEntity getById(Long id); + + public List findByIds(List ids); + + public List findByName(String name); + + public ManufacturerEntity assignBaseSet(ManufacturerEntity comic, BaseSetEntity baseSet); + + public ManufacturerEntity store(ManufacturerEntity entity); + + public void delete(ManufacturerEntity entity); +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/ManufacturerImpl.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/ManufacturerImpl.java new file mode 100644 index 0000000..a072a99 --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/ManufacturerImpl.java @@ -0,0 +1,67 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package com.peetz.tradingcards.dal; + +import com.peetz.tradingcards.entity.BaseSetEntity; +import com.peetz.tradingcards.entity.ManufacturerEntity; +import java.util.List; +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +/** + * + * @author tpeetz + */ +@Stateless(name = "ManufacturerDao") +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class ManufacturerImpl implements ManufacturerDao { + + @PersistenceContext(unitName = "kontor") + private EntityManager em; + + @Override + public ManufacturerEntity getById(Long id) { + Query q = em.createNamedQuery("Manufacturer.findById"); + q.setParameter("id", id); + ManufacturerEntity entity = (ManufacturerEntity)q.getSingleResult(); + return entity; + } + + @Override + public List findByIds(List ids) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @SuppressWarnings("unchecked") + @Override + public List findByName(String name) { + Query q = em.createNamedQuery("Manufacturer.findByName"); + q.setParameter("name", name); + List resultList = q.getResultList(); + return resultList; + } + + @Override + public ManufacturerEntity assignBaseSet(ManufacturerEntity comic, BaseSetEntity baseSet) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public ManufacturerEntity store(ManufacturerEntity entity) { + em.persist(entity); + return entity; + } + + @Override + public void delete(ManufacturerEntity entity) { + em.remove(entity); + } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/SportDao.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/SportDao.java new file mode 100644 index 0000000..badd03c --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/SportDao.java @@ -0,0 +1,28 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package com.peetz.tradingcards.dal; + +import com.peetz.tradingcards.entity.SportEntity; +import java.util.List; +import javax.ejb.Local; + +/** + * + * @author tpeetz + */ +@Local +public interface SportDao { + public SportEntity getById(Long id); + + public List findByIds(List ids); + + public List findByName(String name); + + public SportEntity store(SportEntity entity); + + public void delete(SportEntity entity); +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/SportImpl.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/SportImpl.java new file mode 100644 index 0000000..059f52f --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/SportImpl.java @@ -0,0 +1,58 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package com.peetz.tradingcards.dal; + +import com.peetz.tradingcards.entity.SportEntity; +import java.util.List; +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +/** + * + * @author tpeetz + */ +@Stateless(name = "SportDao") +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class SportImpl implements SportDao { + + @PersistenceContext(unitName = "kontor") + private EntityManager em; + + @Override + public SportEntity getById(Long id) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public List findByIds(List ids) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @SuppressWarnings("unchecked") + @Override + public List findByName(String name) { + Query query = em.createNamedQuery("Sport.findByName"); + query.setParameter("name", name); + List resultList = query.getResultList(); + return resultList; + } + + @Override + public SportEntity store(SportEntity entity) { + em.persist(entity); + return entity; + } + + @Override + public void delete(SportEntity entity) { + em.remove(entity); + } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/BaseSetEntity.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/BaseSetEntity.java new file mode 100644 index 0000000..30a23ac --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/BaseSetEntity.java @@ -0,0 +1,67 @@ +package com.peetz.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="BASESET") +public class BaseSetEntity { + private Long id; + private String name; + private ManufacturerEntity manufacturer; + private Collection parallelSets = new ArrayList(); + private Collection inserts = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ManyToOne + public ManufacturerEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(ManufacturerEntity manufacturer) { + this.manufacturer = manufacturer; + } + + @OneToMany(mappedBy="baseSet", cascade=CascadeType.REMOVE) + public Collection getParallelSets() { + return parallelSets; + } + + public void setParallelSets(Collection parallelSets) { + this.parallelSets = parallelSets; + } + + @OneToMany(mappedBy="baseSet", cascade=CascadeType.REMOVE) + public Collection getInserts() { + return inserts; + } + + public void setInserts(Collection inserts) { + this.inserts = inserts; + } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/InsertEntity.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/InsertEntity.java new file mode 100644 index 0000000..7c44c92 --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/InsertEntity.java @@ -0,0 +1,74 @@ +package com.peetz.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="InsertSet.findAll", query="SELECT i from InsertEntity as i"), + @NamedQuery(name="InsertSet.findById", query="SELECT i from InsertEntity as i WHERE i.id = :id"), + @NamedQuery(name="InsertSet.findByName", query="SELECT i from InsertEntity as i WHERE i.name = :name") +}) + +@Entity +@Table(name="INSERTSET") +public class InsertEntity { + private Long id; + private String name; + private ManufacturerEntity manufacturer; + private BaseSetEntity baseSet; + private Collection sportCard = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ManyToOne + public ManufacturerEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(ManufacturerEntity manufacturer) { + this.manufacturer = manufacturer; + } + + @ManyToOne + public BaseSetEntity getBaseSet() { + return baseSet; + } + + public void setBaseSet(BaseSetEntity baseSet) { + this.baseSet = baseSet; + } + + @OneToMany(mappedBy="insert", cascade=CascadeType.REMOVE) + public Collection getSportCard() { + return sportCard; + } + + public void setSportCard(Collection sportCard) { + this.sportCard = sportCard; + } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/ManufacturerEntity.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/ManufacturerEntity.java new file mode 100644 index 0000000..4d73b06 --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/ManufacturerEntity.java @@ -0,0 +1,74 @@ +package com.peetz.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Manufacturer.findAll", query="SELECT m from ManufacturerEntity as m"), + @NamedQuery(name="Manufacturer.findByName", query="SELECT m from ManufacturerEntity as m WHERE m.name = :name") +}) + +@Entity +@Table(name="MANUFACTURER") +public class ManufacturerEntity { + + private Long id; + private String name; + private Collection baseSets = new ArrayList(); + private Collection parallelSets = new ArrayList(); + private Collection inserts = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @OneToMany(mappedBy="manufacturer", cascade=CascadeType.REMOVE) + public Collection getBaseSets() { + return baseSets; + } + + public void setBaseSets(Collection baseSets) { + this.baseSets = baseSets; + } + + @OneToMany(mappedBy="manufacturer", cascade=CascadeType.REMOVE) + public Collection getParallelSets() { + return parallelSets; + } + + public void setParallelSets(Collection parallelSets) { + this.parallelSets = parallelSets; + } + + @OneToMany(mappedBy="manufacturer", cascade=CascadeType.REMOVE) + public Collection getInserts() { + return inserts; + } + + public void setInserts(Collection inserts) { + this.inserts = inserts; + } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/ParallelSetEntity.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/ParallelSetEntity.java new file mode 100644 index 0000000..2057456 --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/ParallelSetEntity.java @@ -0,0 +1,53 @@ +package com.peetz.tradingcards.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name="PARALLELSET") +public class ParallelSetEntity { + private Long id; + private String name; + private ManufacturerEntity manufacturer; + + private BaseSetEntity baseSet; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + @ManyToOne + public ManufacturerEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(ManufacturerEntity manufacturer) { + this.manufacturer = manufacturer; + } + + public void setName(String name) { + this.name = name; + } + + @ManyToOne + public BaseSetEntity getBaseSet() { + return baseSet; + } + + public void setBaseSet(BaseSetEntity baseSet) { + this.baseSet = baseSet; + } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/PlayerEntity.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/PlayerEntity.java new file mode 100644 index 0000000..2d34b16 --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/PlayerEntity.java @@ -0,0 +1,46 @@ +package com.peetz.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="PLAYER") +public class PlayerEntity { + private Long id; + private TeamEntity team; + private Collection cards = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @ManyToOne + public TeamEntity getTeam() { + return team; + } + + public void setTeam(TeamEntity team) { + this.team = team; + } + + @OneToMany(mappedBy="player", cascade=CascadeType.REMOVE) + public Collection getCards() { + return cards; + } + + public void setCards(Collection cards) { + this.cards = cards; + } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/PositionEntity.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/PositionEntity.java new file mode 100644 index 0000000..b4e1a0b --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/PositionEntity.java @@ -0,0 +1,53 @@ +package com.peetz.tradingcards.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name="POSITION") +public class PositionEntity { + + private Long id; + private String name; + private String shortName; + private SportEntity sport; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Column + public String getShortName() { + return shortName; + } + + public void setShortName(String shortName) { + this.shortName = shortName; + } + + @ManyToOne + public SportEntity getSport() { + return sport; + } + + public void setSport(SportEntity sport) { + this.sport = sport; + } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/SportCardEntity.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/SportCardEntity.java new file mode 100644 index 0000000..fd0e5e4 --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/SportCardEntity.java @@ -0,0 +1,68 @@ +package com.peetz.tradingcards.entity; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="SportCard.findAll", query="SELECT s from SportCardEntity as s") +}) + +@Entity +@Table(name = "SPORTCARD") +public class SportCardEntity { + private Long id; + private PlayerEntity player; + private BaseSetEntity baseSet; + private ParallelSetEntity parallelSet; + private InsertEntity insert; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @ManyToOne + public PlayerEntity getPlayer() { + return player; + } + + public void setPlayer(PlayerEntity player) { + this.player = player; + } + + @ManyToOne + public BaseSetEntity getBaseSet() { + return baseSet; + } + + public void setBaseSet(BaseSetEntity baseSet) { + this.baseSet = baseSet; + } + + @ManyToOne + public ParallelSetEntity getParallelSet() { + return parallelSet; + } + + public void setParallelSet(ParallelSetEntity parallelSet) { + this.parallelSet = parallelSet; + } + + @ManyToOne + public InsertEntity getInsert() { + return insert; + } + + public void setInsert(InsertEntity insert) { + this.insert = insert; + } + +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/SportEntity.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/SportEntity.java new file mode 100644 index 0000000..7574a2d --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/SportEntity.java @@ -0,0 +1,64 @@ +package com.peetz.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Sport.findAll", query="SELECT s from SportEntity as s"), + @NamedQuery(name="Sport.findByName", query="SELECT s from SportEntity as s WHERE s.name = :name") +}) + +@Entity +@Table(name="SPORT") +public class SportEntity { + + private Long id; + private String name; + private Collection teams = new ArrayList(); + private Collection positions = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @OneToMany(mappedBy="sport", cascade=CascadeType.REMOVE) + public Collection getTeams() { + return teams; + } + + public void setTeams(Collection teams) { + this.teams = teams; + } + + @OneToMany(mappedBy="sport", cascade=CascadeType.REMOVE) + public Collection getPositions() { + return positions; + } + + public void setPositions(Collection positions) { + this.positions = positions; + } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/TeamEntity.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/TeamEntity.java new file mode 100644 index 0000000..a0863ea --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/TeamEntity.java @@ -0,0 +1,42 @@ +package com.peetz.tradingcards.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Team.findAll", query="SELECT t from TeamEntity as t"), + @NamedQuery(name="Team.findByName", query="SELECT t from TeamEntity as t WHERE t.name = :name") +}) + +@Entity +@Table(name="TEAM") +public class TeamEntity { + + private Long id; + private String name; + private SportEntity sport; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { return name; } + + public void setName(String name) { this.name = name; } + + @ManyToOne + public SportEntity getSport() { return sport; } + + public void setSport(SportEntity sport) { this.sport = sport; } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/SportService.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/SportService.java new file mode 100644 index 0000000..805101a --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/SportService.java @@ -0,0 +1,24 @@ +package com.peetz.tradingcards.service; + +import java.util.Collection; + +import javax.ejb.Local; + +import com.peetz.tradingcards.entity.PositionEntity; +import com.peetz.tradingcards.entity.SportEntity; +import com.peetz.tradingcards.entity.TeamEntity; + +@Local +public interface SportService { + + Collection getAllSports(); + + Collection getAllTeams(); + + void addSport(String name); + + Collection getTeams(SportEntity sport); + + Collection getPositions(SportEntity sport); + +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/SportServiceImpl.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/SportServiceImpl.java new file mode 100644 index 0000000..1a348ef --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/SportServiceImpl.java @@ -0,0 +1,63 @@ +package com.peetz.tradingcards.service; + +import com.peetz.tradingcards.dal.SportDao; +import java.util.Collection; + +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; + +import com.peetz.tradingcards.entity.PositionEntity; +import com.peetz.tradingcards.entity.SportEntity; +import com.peetz.tradingcards.entity.TeamEntity; +import java.util.ArrayList; +import javax.ejb.EJB; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +@Stateless(name="SportService") +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class SportServiceImpl implements SportService { + + @PersistenceContext(unitName = "kontor") + private EntityManager em; + + @EJB + SportDao sportDao; + + @SuppressWarnings("unchecked") + @Override + public Collection getAllSports() { + Query query = em.createNamedQuery("Sport.findAll"); + ArrayList sportList = new ArrayList(query.getResultList()); + return sportList; + } + + @SuppressWarnings("unchecked") + @Override + public Collection getAllTeams() { + Query query = em.createNamedQuery("Team.findAll"); + ArrayList teamList = new ArrayList(query.getResultList()); + return teamList; + } + + + @Override + public void addSport(String name) { + SportEntity entity = new SportEntity(); + entity.setName(name); + sportDao.store(entity); + } + + @Override + public Collection getTeams(SportEntity sport) { + return null; + } + + @Override + public Collection getPositions(SportEntity sport) { + // TODO Auto-generated method stub + return null; + } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/TradingcardService.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/TradingcardService.java new file mode 100644 index 0000000..9d5b0eb --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/TradingcardService.java @@ -0,0 +1,28 @@ +package com.peetz.tradingcards.service; + +import java.util.Collection; + +import javax.ejb.Local; + +import com.peetz.tradingcards.entity.BaseSetEntity; +import com.peetz.tradingcards.entity.InsertEntity; +import com.peetz.tradingcards.entity.ManufacturerEntity; +import com.peetz.tradingcards.entity.ParallelSetEntity; +import com.peetz.tradingcards.entity.SportCardEntity; + +@Local +public interface TradingcardService { + + Collection getAllManufacturers(); + + void addManufacturer(String name); + + Collection getAllSportCards(); + + Collection getBaseSetsByManufacturer(ManufacturerEntity manufacturer); + + Collection getParallelSetsByManufacturer(ManufacturerEntity manufacturer); + + Collection getInsertsByManufacturer(ManufacturerEntity manufacturer); + +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/TradingcardServiceImpl.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/TradingcardServiceImpl.java new file mode 100644 index 0000000..b8703a4 --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/TradingcardServiceImpl.java @@ -0,0 +1,75 @@ +package com.peetz.tradingcards.service; + +import com.peetz.tradingcards.dal.ManufacturerDao; +import java.util.Collection; + +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; + +import com.peetz.tradingcards.entity.BaseSetEntity; +import com.peetz.tradingcards.entity.InsertEntity; +import com.peetz.tradingcards.entity.ManufacturerEntity; +import com.peetz.tradingcards.entity.ParallelSetEntity; +import com.peetz.tradingcards.entity.SportCardEntity; +import java.util.ArrayList; +import java.util.List; +import javax.ejb.EJB; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +@Stateless(name="TradingcardService") +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class TradingcardServiceImpl implements TradingcardService { + + @PersistenceContext(unitName = "kontor") + private EntityManager em; + + @EJB + ManufacturerDao manufacturerDao; + + @SuppressWarnings("unchecked") + @Override + public Collection getAllManufacturers() { + Query query = em.createNamedQuery("Manufacturer.findAll"); + ArrayList manufacturerList = new ArrayList(query.getResultList()); + return manufacturerList; + } + + @Override + public void addManufacturer(String name) { + List resultList = manufacturerDao.findByName(name); + if (resultList.isEmpty()) { + ManufacturerEntity manufacturer = new ManufacturerEntity(); + manufacturer.setName(name); + manufacturerDao.store(manufacturer); + } + } + + @SuppressWarnings("unchecked") + @Override + public Collection getAllSportCards() { + Query query = em.createNamedQuery("SportCard.findAll"); + ArrayList sportCardList = new ArrayList(query.getResultList()); + return sportCardList; + } + + @Override + public Collection getBaseSetsByManufacturer(ManufacturerEntity manufacturer) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Collection getParallelSetsByManufacturer(ManufacturerEntity manufacturer) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Collection getInsertsByManufacturer(ManufacturerEntity manufacturer) { + // TODO Auto-generated method stub + return null; + } +} \ No newline at end of file diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/package-info.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/package-info.java new file mode 100644 index 0000000..f0eb37b --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/package-info.java @@ -0,0 +1,5 @@ +/** + * @author TPEETZ + * + */ +package com.peetz.tradingcards.service; \ No newline at end of file diff --git a/java-ee/TradingCardsImpl/src/test/java/com/peetz/tradingcards/dal/ManufacturerImplTest.java b/java-ee/TradingCardsImpl/src/test/java/com/peetz/tradingcards/dal/ManufacturerImplTest.java new file mode 100644 index 0000000..2e5b5e1 --- /dev/null +++ b/java-ee/TradingCardsImpl/src/test/java/com/peetz/tradingcards/dal/ManufacturerImplTest.java @@ -0,0 +1,147 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package com.peetz.tradingcards.dal; + +import com.peetz.tradingcards.entity.BaseSetEntity; +import com.peetz.tradingcards.entity.ManufacturerEntity; +import java.util.Collection; +import java.util.List; +import javax.ejb.embeddable.EJBContainer; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * + * @author tpeetz + */ +public class ManufacturerImplTest { + + public ManufacturerImplTest() { + } + + @BeforeClass + public static void setUpClass() { + } + + @AfterClass + public static void tearDownClass() { + } + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + /** + * Test of getById method, of class ManufacturerImpl. + */ + @Test + public void testGetById() throws Exception { + System.out.println("getById"); + Long id = null; + EJBContainer container = javax.ejb.embeddable.EJBContainer.createEJBContainer(); + ManufacturerDao instance = (ManufacturerDao)container.getContext().lookup("java:global/main/ManufacturerDao"); + ManufacturerEntity expResult = null; + ManufacturerEntity result = instance.getById(id); + assertEquals(expResult, result); + container.close(); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of findByIds method, of class ManufacturerImpl. + */ + @Test + public void testFindByIds() throws Exception { + System.out.println("findByIds"); + List ids = null; + EJBContainer container = javax.ejb.embeddable.EJBContainer.createEJBContainer(); + ManufacturerDao instance = (ManufacturerDao)container.getContext().lookup("java:global/classes/ManufacturerImpl"); + Collection expResult = null; + Collection result = instance.findByIds(ids); + assertEquals(expResult, result); + container.close(); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of findByName method, of class ManufacturerImpl. + */ + @Test + public void testFindByName() throws Exception { + System.out.println("findByName"); + String name = ""; + EJBContainer container = javax.ejb.embeddable.EJBContainer.createEJBContainer(); + ManufacturerDao instance = (ManufacturerDao)container.getContext().lookup("java:global/classes/ManufacturerImpl"); + Collection expResult = null; + Collection result = instance.findByName(name); + assertEquals(expResult, result); + container.close(); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of assignBaseSet method, of class ManufacturerImpl. + */ + @Test + public void testAssignBaseSet() throws Exception { + System.out.println("assignBaseSet"); + ManufacturerEntity comic = null; + BaseSetEntity baseSet = null; + EJBContainer container = javax.ejb.embeddable.EJBContainer.createEJBContainer(); + ManufacturerDao instance = (ManufacturerDao)container.getContext().lookup("java:global/classes/ManufacturerImpl"); + ManufacturerEntity expResult = null; + ManufacturerEntity result = instance.assignBaseSet(comic, baseSet); + assertEquals(expResult, result); + container.close(); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of store method, of class ManufacturerImpl. + */ + @Test + public void testStore() throws Exception { + System.out.println("store"); + ManufacturerEntity entity = null; + EJBContainer container = javax.ejb.embeddable.EJBContainer.createEJBContainer(); + ManufacturerDao instance = (ManufacturerDao)container.getContext().lookup("java:global/classes/ManufacturerImpl"); + ManufacturerEntity expResult = null; + ManufacturerEntity result = instance.store(entity); + assertEquals(expResult, result); + container.close(); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of delete method, of class ManufacturerImpl. + */ + @Test + public void testDelete() throws Exception { + System.out.println("delete"); + ManufacturerEntity entity = null; + EJBContainer container = javax.ejb.embeddable.EJBContainer.createEJBContainer(); + ManufacturerDao instance = (ManufacturerDao)container.getContext().lookup("java:global/classes/ManufacturerImpl"); + instance.delete(entity); + container.close(); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + +} diff --git a/java-ee/TradingCardsWeb/build.gradle b/java-ee/TradingCardsWeb/build.gradle new file mode 100644 index 0000000..baa6136 --- /dev/null +++ b/java-ee/TradingCardsWeb/build.gradle @@ -0,0 +1,7 @@ +apply plugin: 'war' + +version = '0.0.1' + +dependencies { + compile project(':TradingCardsImpl') +} diff --git a/java-ee/TradingCardsWeb/src/main/java/com/peetz/tradingcards/view/SportView.java b/java-ee/TradingCardsWeb/src/main/java/com/peetz/tradingcards/view/SportView.java new file mode 100644 index 0000000..b95ca0b --- /dev/null +++ b/java-ee/TradingCardsWeb/src/main/java/com/peetz/tradingcards/view/SportView.java @@ -0,0 +1,52 @@ +package com.peetz.tradingcards.view; + +import com.peetz.tradingcards.dal.SportDao; +import com.peetz.tradingcards.service.SportService; +import java.io.Serializable; +import java.util.logging.Logger; +import javax.ejb.EJB; +import javax.faces.bean.ManagedBean; +import javax.faces.bean.RequestScoped; + +/** + * + * @author tpeetz + */ +@ManagedBean(name="TradingCardsView") +@RequestScoped +public class SportView implements Serializable { + + private static final long serialVersionUID = 1399103334723066025L; + + private static final Logger LOG = Logger.getLogger(SportView.class.getName()); + + private String name; + + @EJB + private SportService sportService; + + public SportView() { + LOG.info("SportView created"); + } + + public Integer getSportNumber() { + LOG.info("SportView#getSportNumber"); + return sportService.getAllSports().size(); + } + + public Integer getTeamNumber() { + LOG.info("SportView#getTeamNumber"); + return sportService.getAllTeams().size(); + } + + public String getName() { + LOG.info("SportView#getName"); + return name; + } + + public void setName(String name) { + this.name = name; + sportService.addSport(name); + LOG.info("SportView#setName"); + } +} diff --git a/java-ee/TradingCardsWeb/src/main/java/com/peetz/tradingcards/view/TradingCardsView.java b/java-ee/TradingCardsWeb/src/main/java/com/peetz/tradingcards/view/TradingCardsView.java new file mode 100644 index 0000000..e1d5879 --- /dev/null +++ b/java-ee/TradingCardsWeb/src/main/java/com/peetz/tradingcards/view/TradingCardsView.java @@ -0,0 +1,36 @@ +package com.peetz.tradingcards.view; + +import com.peetz.tradingcards.service.TradingcardService; +import java.io.Serializable; +import java.util.logging.Logger; +import javax.ejb.EJB; +import javax.faces.bean.ManagedBean; +import javax.faces.bean.RequestScoped; + +/** + * + * @author TPEETZ + */ +@ManagedBean(name="TradingCardsView") +@RequestScoped +public class TradingCardsView implements Serializable { + + private static final Logger LOG = Logger.getLogger(TradingCardsView.class.getName()); + + @EJB + private TradingcardService tradingcardService; + + private static final long serialVersionUID = -8261128991042235283L; + + public TradingCardsView() { + LOG.info("TradingCardsView created"); + } + + public Integer getManufacturerNumber() { + return tradingcardService.getAllManufacturers().size(); + } + + public Integer getSportCardNumber() { + return tradingcardService.getAllSportCards().size(); + } +} diff --git a/java-ee/TradingCardsWeb/src/main/webapp/index.jsp b/java-ee/TradingCardsWeb/src/main/webapp/index.jsp new file mode 100644 index 0000000..d085601 --- /dev/null +++ b/java-ee/TradingCardsWeb/src/main/webapp/index.jsp @@ -0,0 +1,33 @@ + + TradingCards Application + + + + + + + + + + + + + + + + + + + + + + + +
TradingCards Manager
test +

TradingCards Manager

+ Show the card list +
 
+

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/java-ee/build.gradle b/java-ee/build.gradle new file mode 100644 index 0000000..e8b1bd8 --- /dev/null +++ b/java-ee/build.gradle @@ -0,0 +1,69 @@ +allprojects { + apply plugin: 'java' + apply plugin: 'build-dashboard' + + repositories { + mavenLocal() + mavenCentral() + } + version = '0.0.1' +} + +repositories { + mavenLocal() + mavenCentral() +} + +group = 'com.ibtp.kontor' + +dependencies { + compile 'org.jboss.spec.javax.faces:jboss-jsf-api_2.2_spec:+' + compile 'org.hibernate.ogm:hibernate-ogm-mongodb:4.2.+' + compile "ch.qos.logback:logback-classic:1.1.3" + compile "org.slf4j:log4j-over-slf4j:1.7.13" + compile "javax:javaee-api:7.0" +} + +subprojects { project -> + if (project.name.endsWith('Impl')) { + apply plugin: 'checkstyle' + apply plugin: 'findbugs' + apply plugin: 'pmd' + apply plugin: 'jacoco' + dependencies { + compile 'org.glassfish.main.ejb:javax.ejb:3.1.2.2' + compile 'org.glassfish.main.transaction:javax.transaction:3.1.2.2' + compile 'org.glassfish:javax.faces:2.1.6' + compile 'org.eclipse.persistence:javax.persistence:2.1.0' + compile 'org.eclipse.persistence:eclipselink:2.5.1' + compile 'org.hibernate:hibernate-core:4.3.8.Final' + compile 'org.hibernate:hibernate-entitymanager:4.3.8.Final' + compile 'org.hsqldb:hsqldb:2.3.0' + compile 'ch.qos.logback:logback-core:1.1.2' + compile 'ch.qos.logback:logback-classic:1.1.2' + testCompile 'org.glassfish.main.extras:glassfish-embedded-all:3.1.2.2' + testCompile group: 'junit', name: 'junit', version: '4.11' + } + tasks.withType(Checkstyle) { + ignoreFailures = true + showViolations = false + reports { + xml.enabled true + } + } + tasks.withType(FindBugs) { + reports { + xml.enabled true + } + } + pmd { + ignoreFailures = true + } + build.dependsOn(['jacocoTestReport']) + } +} + +wrapper { + gradleVersion = '3.3' +} + diff --git a/java-ee/comics.xml b/java-ee/comics.xml new file mode 100644 index 0000000..730fdeb --- /dev/null +++ b/java-ee/comics.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java-ee/config/checkstyle/checkstyle.xml b/java-ee/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..7c682c3 --- /dev/null +++ b/java-ee/config/checkstyle/checkstyle.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java-ee/config/checkstyle/checkstyle.xsl b/java-ee/config/checkstyle/checkstyle.xsl new file mode 100644 index 0000000..393a01b --- /dev/null +++ b/java-ee/config/checkstyle/checkstyle.xsl @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

CheckStyle Audit

Designed for use with CheckStyle and Ant.
+
+ + + +
+ + + +
+ + + + + +

+

+ +


+ + + + +
+ + + + +

Files

+ + + + + + + + + + + + + + +
NameErrors
+
+ + + + +

File

+ + + + + + + + + + + + + +
Error DescriptionLine
+ Back to top +
+ + + +

Summary

+ + + + + + + + + + + + +
FilesErrors
+
+ + + + a + b + + +
+ + diff --git a/java-ee/config/findbugs/findbugs.xml b/java-ee/config/findbugs/findbugs.xml new file mode 100644 index 0000000..34a6e01 --- /dev/null +++ b/java-ee/config/findbugs/findbugs.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java-ee/gradle/wrapper/gradle-wrapper.jar b/java-ee/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..8ba921dbd16c876b27af7388dcbd710bdd096612 GIT binary patch literal 54208 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/java-ee/gradlew.bat b/java-ee/gradlew.bat new file mode 100755 index 0000000..e95643d --- /dev/null +++ b/java-ee/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/java-ee/settings.gradle b/java-ee/settings.gradle new file mode 100755 index 0000000..50ec7b4 --- /dev/null +++ b/java-ee/settings.gradle @@ -0,0 +1,13 @@ +include ':KontorEJB' +include ':KontorWeb' +include ':ComicsImpl' +include ':ComicsWeb' +include ':LibraryImpl' +include ':LibraryWeb' +include ':MedienImpl' +include ':MedienWeb' +include ':TradingCardsImpl' +include ':TradingCardsWeb' +include ':KontorImpl' +include ':KontorWeb' +include ':KontorApp' diff --git a/java/README.md b/java/README.md new file mode 100644 index 0000000..65ed57c --- /dev/null +++ b/java/README.md @@ -0,0 +1,2 @@ +# Kontor Java + diff --git a/java/build.gradle b/java/build.gradle new file mode 100644 index 0000000..1a683be --- /dev/null +++ b/java/build.gradle @@ -0,0 +1,87 @@ +plugins { + id 'application' + id 'jacoco' + id "org.sonarqube" version "3.3" +} + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation 'ch.qos.logback:logback-core:1.1.2' + implementation 'ch.qos.logback:logback-classic:1.1.2' + implementation 'org.mongodb.morphia:morphia:1.1.0' + //compile 'org.mongodb:mongodb-driver:3.2.2' + implementation 'org.hibernate:hibernate-core:4.3.8.Final' + implementation 'org.hibernate:hibernate-entitymanager:4.3.8.Final' + implementation 'org.hsqldb:hsqldb:2.3.0' + implementation 'ch.qos.logback:logback-core:1.1.2' + implementation 'ch.qos.logback:logback-classic:1.1.2' + testImplementation("org.junit.jupiter:junit-jupiter:5.8.2") +} + +def MAIN_CLASS_NAME = 'com.ibtp.kontor.KontorApp' + +application { + mainClassName = MAIN_CLASS_NAME +} + +jar { + manifest { + attributes('Implementation-Title': 'Kontor Application', 'Implementation-Version': version, 'Main-Class': MAIN_CLASS_NAME) + } +} + +tasks.named('test') { + useJUnitPlatform() +} + +jacocoTestReport { + reports { + xml.enabled true + } +} + +test.finalizedBy jacocoTestReport + +sonarqube { + properties { + property "sonar.projectKey", "kontor_kontor-java_AX-dd-w3rXuu6JVRvr_g" + property "sonar.host.url", "https://sonar.thpeetz.de" + property "sonar.login", "d39622f640a91f501b1e8a73d7d78c4fc412fc98" + property "sonar.qualitygate.wait", true + property "sonar.sourceEncoding", "UTF-8" + } +} + +tasks.named('sonarqube').configure { + dependsOn test +} + +//tasks.withType(Checkstyle) { +// ignoreFailures = true +// showViolations = false +// configFile = rootProject.file("config/checkstyle/checkstyle.xml") +// reports { +// xml.enabled true +// } +//} + +//tasks.withType(FindBugs) { +// ignoreFailures = true +// reports { +// xml.enabled true +// } +//} + +//pmd { +// ignoreFailures = true +//} + +//build.dependsOn(['jacocoTestReport']) + +wrapper { + gradleVersion = "6.3" +} diff --git a/java/comics.xml b/java/comics.xml new file mode 100644 index 0000000..730fdeb --- /dev/null +++ b/java/comics.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java/config/checkstyle/checkstyle.xml b/java/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..7c682c3 --- /dev/null +++ b/java/config/checkstyle/checkstyle.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/config/checkstyle/checkstyle.xsl b/java/config/checkstyle/checkstyle.xsl new file mode 100644 index 0000000..393a01b --- /dev/null +++ b/java/config/checkstyle/checkstyle.xsl @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

CheckStyle Audit

Designed for use with CheckStyle and Ant.
+
+ + + +
+ + + +
+ + + + + +

+

+ +


+ + + + +
+ + + + +

Files

+ + + + + + + + + + + + + + +
NameErrors
+
+ + + + +

File

+ + + + + + + + + + + + + +
Error DescriptionLine
+ Back to top +
+ + + +

Summary

+ + + + + + + + + + + + +
FilesErrors
+
+ + + + a + b + + +
+ + diff --git a/java/config/findbugs/findbugs.xml b/java/config/findbugs/findbugs.xml new file mode 100644 index 0000000..34a6e01 --- /dev/null +++ b/java/config/findbugs/findbugs.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java/gradle.properties b/java/gradle.properties new file mode 100644 index 0000000..4facf35 --- /dev/null +++ b/java/gradle.properties @@ -0,0 +1,2 @@ +description = 'Application kontor-java' +version = '1.0.0-SNAPSHOT' diff --git a/java/gradle/wrapper/gradle-wrapper.jar b/java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/java/gradle/wrapper/gradle-wrapper.properties b/java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a4b4429 --- /dev/null +++ b/java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/java/gradlew b/java/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/java/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/java/gradlew.bat b/java/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/java/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/java/settings.gradle b/java/settings.gradle new file mode 100644 index 0000000..d7360d1 --- /dev/null +++ b/java/settings.gradle @@ -0,0 +1 @@ +rootProject.name='kontor-java' diff --git a/java/src/main/java/com/ibtp/kontor/Database.java b/java/src/main/java/com/ibtp/kontor/Database.java new file mode 100644 index 0000000..8d97843 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/Database.java @@ -0,0 +1,46 @@ +package com.ibtp.kontor; + +import com.mongodb.MongoClient; +import org.mongodb.morphia.Datastore; +import org.mongodb.morphia.Morphia; + +import java.util.HashMap; + +/** + * Created by thomas on 01.03.16. + */ +public class Database { + + private static Database instance = null; + + private HashMap dbMap; + private Morphia morphia; + + private Database() { + dbMap = new HashMap(); + initMorphia(); + } + + private final void initMorphia() { + morphia = new Morphia(); + morphia.mapPackage("com.ibtp.kontor.comics.entity"); + } + + public void registerDatastore(String databaseName) { + if (!dbMap.containsKey(databaseName)) { + Datastore store = morphia.createDatastore(new MongoClient("127.0.0.1"), databaseName); + dbMap.put(databaseName, store); + } + } + + public Datastore getDatastore(String databaseName) { + return dbMap.get(databaseName); + } + + public static Database init() { + if (instance == null) { + instance = new Database(); + } + return instance; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/DumpComics.java b/java/src/main/java/com/ibtp/kontor/DumpComics.java new file mode 100644 index 0000000..6ee56b1 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/DumpComics.java @@ -0,0 +1,51 @@ +package com.ibtp.kontor; + +import com.ibtp.kontor.comics.entity.Artist; +import com.ibtp.kontor.comics.entity.Comic; +import com.ibtp.kontor.comics.entity.Publisher; +import com.mongodb.MongoClient; +import org.mongodb.morphia.Datastore; +import org.mongodb.morphia.Morphia; +import org.mongodb.morphia.query.Query; + +import java.util.Iterator; +import java.util.List; + +/** + * Created by thomas on 29.02.16. + */ +public class DumpComics { + + public static void main(String[] args) { + final Database database = Database.init(); + database.registerDatastore("kontor"); + database.registerDatastore("comics"); + Datastore kontor = database.getDatastore("kontor"); + Datastore comics = database.getDatastore("comics"); + final Query query = kontor.createQuery(Artist.class); + final List artists = query.asList(); + System.out.println(artists); + for (Artist artist: artists) { + String artistName = artist.getName(); + System.out.println("Artist(" + artist.getId() + ": " + artistName + ")"); + if (comics.createQuery(Artist.class).field("name").equal(artistName).asList().isEmpty()) { + Artist comicsArtist = new Artist(artistName); + comics.save(comicsArtist); + } + } + final List publishers = kontor.createQuery(Publisher.class).asList(); + for (Publisher publisher: publishers) { + String publisherName = publisher.getName(); + System.out.println("Publisher(" + publisher.getId() + ": " + publisherName + ")"); + if (comics.createQuery(Publisher.class).field("name").equal(publisherName).asList().isEmpty()) { + Publisher comicsPublisher = new Publisher(publisherName); + comics.save(comicsPublisher); + } + } + final List comicList = comics.createQuery(Comic.class).asList(); + System.out.println(comicList); + for (Comic comic: comicList ) { + System.out.println(comic); + } + } +} diff --git a/java/src/main/java/com/ibtp/kontor/KontorApp.java b/java/src/main/java/com/ibtp/kontor/KontorApp.java new file mode 100644 index 0000000..b87a849 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/KontorApp.java @@ -0,0 +1,23 @@ +package com.ibtp.kontor; + +/** + * Created by TPEETZ on 10.02.2015. + */ +public class KontorApp { + + private KontorGUI mainframe; + + public KontorApp() { + mainframe = new KontorGUI(this); + + mainframe.setVisible(true); + } + + public void exitApplication() { + System.exit(0); + } + + public static void main(String[] args) { + new KontorApp(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/KontorGUI.java b/java/src/main/java/com/ibtp/kontor/KontorGUI.java new file mode 100644 index 0000000..40a4b2d --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/KontorGUI.java @@ -0,0 +1,69 @@ +package com.ibtp.kontor; + + +import com.ibtp.kontor.comics.view.ComicsMenu; +import com.ibtp.kontor.library.view.LibraryMenu; +import com.ibtp.kontor.tradingcards.view.TradingCardsMenu; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +/** + * Created by TPEETZ on 11.02.2015. + */ +public class KontorGUI extends javax.swing.JFrame { + + KontorApp application; + JMenuBar menuBar; + JMenu menuFile; + JMenuItem menuFileExit; + + JMenuItem menuFileStart = new JMenuItem(); + + JMenu menuHelp; + JMenuItem menuHelpAbout; + + public KontorGUI(KontorApp kontorApp) { + application = kontorApp; + initComponents(); + } + + private void initComponents() { + setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + GroupLayout layout = new GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(GroupLayout.Alignment.LEADING).addGap(0, 400, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(GroupLayout.Alignment.LEADING).addGap(0, 300, Short.MAX_VALUE) + ); + pack(); + setTitle("Kontor Application"); + createMainMenu(); + //createToolBar(); + } + + private void createMainMenu() { + menuBar = new JMenuBar(); + menuFile = new JMenu("File"); + menuFileExit = new JMenuItem("Exit"); + menuHelp = new JMenu("Help"); + menuHelpAbout = new JMenuItem("About"); + setJMenuBar(menuBar); + menuBar.add(menuFile); + menuFile.add(menuFileExit); + menuBar.add(new ComicsMenu()); + menuBar.add(new LibraryMenu()); + menuBar.add(new TradingCardsMenu()); + menuBar.add(menuHelp); + menuHelp.add(menuHelpAbout); + menuFileExit.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + application.exitApplication(); + } + }); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/ArtistDao.java b/java/src/main/java/com/ibtp/kontor/comics/dal/ArtistDao.java new file mode 100644 index 0000000..00c4b01 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/ArtistDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ArtistEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 16.01.2015. + */ +interface ArtistDao { + + public ArtistEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public Collection findAll(); + + public ArtistEntity addArtist(String name); + + public ArtistEntity store(ArtistEntity entity); + + public void delete(ArtistEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/ArtistImpl.java b/java/src/main/java/com/ibtp/kontor/comics/dal/ArtistImpl.java new file mode 100644 index 0000000..d01b987 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/ArtistImpl.java @@ -0,0 +1,68 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ArtistEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 16.01.2015. + */ +public class ArtistImpl extends BaseImpl implements ArtistDao { + + public ArtistImpl() {} + + @Override + public ArtistEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Artist.findById"); + query.setParameter("id", id); + return (ArtistEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Artist.findAll"); + return query.getResultList(); + + } + + @Override + public Collection findByName(String name) { + Query query = getEntityManager().createNamedQuery("Artist.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public ArtistEntity addArtist(String name) { + ArtistEntity artist = new ArtistEntity(name); + artist = store(artist); + return artist; + } + + @Override + public ArtistEntity store(ArtistEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(ArtistEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/ComicDao.java b/java/src/main/java/com/ibtp/kontor/comics/dal/ComicDao.java new file mode 100644 index 0000000..c77fb89 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/ComicDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ComicEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by thomas on 17.01.15. + */ +interface ComicDao { + + public ComicEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByTitle(String title); + + public Collection findAll(); + + public ComicEntity addComic(String title); + + public ComicEntity store(ComicEntity entity); + + public void delete(ComicEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/ComicImpl.java b/java/src/main/java/com/ibtp/kontor/comics/dal/ComicImpl.java new file mode 100644 index 0000000..ecbad88 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/ComicImpl.java @@ -0,0 +1,65 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ComicEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class ComicImpl extends BaseImpl implements ComicDao { + + @Override + public ComicEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Comic.findById"); + query.setParameter("id", id); + return (ComicEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByTitle(String title) { + Query query = getEntityManager().createNamedQuery("Comic.findByTitle"); + query.setParameter("title", title); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Comic.findAll"); + return query.getResultList(); + } + + @Override + public ComicEntity addComic(String title) { + ComicEntity comicEntity = new ComicEntity(); + comicEntity.setTitle(title); + store(comicEntity); + return comicEntity; + } + + @Override + public ComicEntity store(ComicEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(ComicEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/IssueDao.java b/java/src/main/java/com/ibtp/kontor/comics/dal/IssueDao.java new file mode 100644 index 0000000..49a5ab2 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/IssueDao.java @@ -0,0 +1,23 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.IssueEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +interface IssueDao { + public IssueEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByNumber(String number); + + public Collection findAll(); + + public IssueEntity store(IssueEntity entity); + + public void delete(IssueEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/IssueImpl.java b/java/src/main/java/com/ibtp/kontor/comics/dal/IssueImpl.java new file mode 100644 index 0000000..8f70845 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/IssueImpl.java @@ -0,0 +1,57 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.IssueEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class IssueImpl extends BaseImpl implements IssueDao { + + @Override + public IssueEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Issue.findById"); + query.setParameter("id", id); + return (IssueEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByNumber(String number) { + Query query = getEntityManager().createNamedQuery("Issue.findByNumber"); + query.setParameter("number", number); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Issue.findAll"); + return query.getResultList(); + } + + @Override + public IssueEntity store(IssueEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(IssueEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/PublisherDao.java b/java/src/main/java/com/ibtp/kontor/comics/dal/PublisherDao.java new file mode 100644 index 0000000..0722522 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/PublisherDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.PublisherEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by thomas on 17.01.15. + */ +interface PublisherDao { + + public PublisherEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public Collection findAll(); + + public PublisherEntity addPublisher(String name); + + public PublisherEntity store(PublisherEntity entity); + + public void delete(PublisherEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/PublisherImpl.java b/java/src/main/java/com/ibtp/kontor/comics/dal/PublisherImpl.java new file mode 100644 index 0000000..d911c4d --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/PublisherImpl.java @@ -0,0 +1,62 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.PublisherEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 20.01.2015. + */ +public class PublisherImpl extends BaseImpl implements PublisherDao { + + @Override + public PublisherEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + Query query = getEntityManager().createNamedQuery("Publisher.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Publisher.findAll"); + return query.getResultList(); + } + + @Override + public PublisherEntity addPublisher(String name) { + PublisherEntity publisher = new PublisherEntity(name); + store(publisher); + return publisher; + } + + @Override + public PublisherEntity store(PublisherEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(PublisherEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/StoryArcDao.java b/java/src/main/java/com/ibtp/kontor/comics/dal/StoryArcDao.java new file mode 100644 index 0000000..40761fe --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/StoryArcDao.java @@ -0,0 +1,24 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.StoryArcEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +interface StoryArcDao { + + public StoryArcEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByTitle(String title); + + public Collection findAll(); + + public StoryArcEntity store(StoryArcEntity entity); + + public void delete(StoryArcEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/StoryArcImpl.java b/java/src/main/java/com/ibtp/kontor/comics/dal/StoryArcImpl.java new file mode 100644 index 0000000..3a39271 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/StoryArcImpl.java @@ -0,0 +1,55 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.StoryArcEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class StoryArcImpl extends BaseImpl implements StoryArcDao { + + @Override + public StoryArcEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByTitle(String title) { + Query query = getEntityManager().createNamedQuery("StoryArc.findByTitle"); + query.setParameter("title", title); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("StoryArc.findAll"); + return query.getResultList(); + } + + @Override + public StoryArcEntity store(StoryArcEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(StoryArcEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/VolumeDao.java b/java/src/main/java/com/ibtp/kontor/comics/dal/VolumeDao.java new file mode 100644 index 0000000..26759bd --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/VolumeDao.java @@ -0,0 +1,24 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.VolumeEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +interface VolumeDao { + + public VolumeEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByTitle(String title); + + public Collection findAll(); + + public VolumeEntity store(VolumeEntity entity); + + public void delete(VolumeEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/VolumeImpl.java b/java/src/main/java/com/ibtp/kontor/comics/dal/VolumeImpl.java new file mode 100644 index 0000000..bb20a4a --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/VolumeImpl.java @@ -0,0 +1,57 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.VolumeEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class VolumeImpl extends BaseImpl implements VolumeDao { + + @Override + public VolumeEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Volume.findById"); + query.setParameter("id", id); + return (VolumeEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByTitle(String title) { + Query query = getEntityManager().createNamedQuery("Volume.findByTitle"); + query.setParameter("title", title); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Volume.findAll"); + return query.getResultList(); + } + + @Override + public VolumeEntity store(VolumeEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(VolumeEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/Artist.java b/java/src/main/java/com/ibtp/kontor/comics/entity/Artist.java new file mode 100644 index 0000000..a7c09b9 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/Artist.java @@ -0,0 +1,46 @@ +package com.ibtp.kontor.comics.entity; + +import org.bson.types.ObjectId; +import org.mongodb.morphia.annotations.Entity; +import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Property; + +/** + * Created by thomas on 29.02.16. + */ +@Entity("artist") +public class Artist { + + public Artist() {} + + public Artist(String name) { + setName(name); + } + + @Id + private ObjectId id; + + @Property + private String name; + + public ObjectId getId() { + return id; + } + + public void setId(ObjectId id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/ArtistEntity.java b/java/src/main/java/com/ibtp/kontor/comics/entity/ArtistEntity.java new file mode 100644 index 0000000..50ede83 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/ArtistEntity.java @@ -0,0 +1,72 @@ +package com.ibtp.kontor.comics.entity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by TPEETZ on 16.01.2015. + */ +@NamedQueries({ + @NamedQuery(name="Artist.findAll", query="SELECT a from ArtistEntity as a"), + @NamedQuery(name="Artist.findByName", query="SELECT a from ArtistEntity as a WHERE a.name = :name") +}) + +@Entity +@Table(name="ARTIST") +public class ArtistEntity { + + private Long id; + + private String name; + + private Collection writtenIssues = new ArrayList(); + + private Collection inkedIssues = new ArrayList(); + + private Collection penciledIssues = new ArrayList(); + + public ArtistEntity(String name) { + setName(name); + } + + public ArtistEntity() {} + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + String getName() { return name; } + + void setName(String name) { this.name = name; } + + public void setWrittenIssues(Collection writtenIssues) { this.writtenIssues = writtenIssues; } + + @OneToMany(mappedBy="writer", cascade=CascadeType.REMOVE) + public Collection getWrittenIssues() { + return writtenIssues; + } + + public void setInkedIssues(Collection inkedIssues) { this.inkedIssues = inkedIssues; } + + @OneToMany(mappedBy="inker", cascade=CascadeType.REMOVE) + public Collection getInkedIssues() { + return inkedIssues; + } + + public void setPenciledIssues(Collection penciledIssues) { this.penciledIssues = penciledIssues; } + + @OneToMany(mappedBy="penciler", cascade=CascadeType.REMOVE) + public Collection getPenciledIssues() { + return penciledIssues; + } + + @Override + public String toString() { + return "Artist[" + "id=" + getId() + ",name=" + getName() + "]"; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/Comic.java b/java/src/main/java/com/ibtp/kontor/comics/entity/Comic.java new file mode 100644 index 0000000..c6ac7b6 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/Comic.java @@ -0,0 +1,98 @@ +package com.ibtp.kontor.comics.entity; + +import org.bson.types.ObjectId; +import org.mongodb.morphia.annotations.Entity; +import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Property; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by thomas on 01.03.16. + */ +@Entity("comic") +public class Comic { + + @Id + private ObjectId id; + + @Property + private String title; + + @Property + private ObjectId publisher; + + @Property + private Boolean current_order; + + @Property + private Boolean completed; + + @Property + private List issues = new ArrayList(); + + @Property + private List stories = new ArrayList(); + + public ObjectId getId() { + return id; + } + + public void setId(ObjectId id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public ObjectId getPublisher() { + return publisher; + } + + public void setPublisher(ObjectId publisher) { + this.publisher = publisher; + } + + public Boolean getCurrent_order() { + return current_order; + } + + public void setCurrent_order(Boolean current_order) { + this.current_order = current_order; + } + + public Boolean getCompleted() { + return completed; + } + + public void setCompleted(Boolean completed) { + this.completed = completed; + } + + public List getIssues() { + return issues; + } + + public void setIssues(List issues) { + this.issues = issues; + } + + public List getStories() { + return stories; + } + + public void setStories(List stories) { + this.stories = stories; + } + + @Override + public String toString() { + return title + " - " + Publisher.getById(publisher); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/ComicEntity.java b/java/src/main/java/com/ibtp/kontor/comics/entity/ComicEntity.java new file mode 100644 index 0000000..2d21c10 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/ComicEntity.java @@ -0,0 +1,81 @@ +package com.ibtp.kontor.comics.entity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by thomas on 17.01.15. + */ +@NamedQueries({ + @NamedQuery(name="Comic.findAll", query="SELECT c from ComicEntity as c"), + @NamedQuery(name="Comic.findByTitle", query="SELECT c from ComicEntity as c WHERE c.title = :title") +}) +@Entity +@Table(name = "COMIC") +public class ComicEntity { + + private Long id; + + private String title; + + private Boolean completed; + + private Boolean currentOrder; + + private Collection issues = new ArrayList(); + + private Collection storyArc = new ArrayList(); + + private Collection volumes = new ArrayList(); + + private PublisherEntity publisher; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + @Column + public Boolean getCompleted() { return completed; } + + public Boolean isCompleted() { return completed; } + + public void setCompleted(Boolean completed) { this.completed = completed; } + + @Column + public Boolean getCurrentOrder() { return currentOrder; } + + public Boolean isCurrentOrder() { return currentOrder; } + + public void setCurrentOrder(Boolean currentOrder) { this.currentOrder = currentOrder; } + + public void setIssues(Collection issues) { this.issues = issues; } + + @OneToMany(mappedBy="comic", cascade=CascadeType.REMOVE) + public Collection getIssues() { return issues; } + + public void setStoryArc(Collection storyArc) { this.storyArc = storyArc; } + + @OneToMany(mappedBy="comic", cascade=CascadeType.REMOVE) + public Collection getStoryArc() { return storyArc; } + + public void setVolumes(Collection volumes) { this.volumes = volumes; } + + @OneToMany(mappedBy="comic", cascade=CascadeType.REMOVE) + public Collection getVolumes() { return volumes; } + + @ManyToOne + public PublisherEntity getPublisher() { return publisher; } + + public void setPublisher(PublisherEntity publisher) { + this.publisher = publisher; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/Issue.java b/java/src/main/java/com/ibtp/kontor/comics/entity/Issue.java new file mode 100644 index 0000000..9821134 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/Issue.java @@ -0,0 +1,66 @@ +package com.ibtp.kontor.comics.entity; + +import org.bson.types.ObjectId; +import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Property; + +/** + * Created by thomas on 01.03.16. + */ +public class Issue { + + @Id + private ObjectId id; + + @Property + private String number; + + //@Reference + private Comic comic; + + @Property + private Boolean is_read; + + @Property + private Boolean is_stock; + + public ObjectId getId() { + return id; + } + + public void setId(ObjectId id) { + this.id = id; + } + + public String getNumber() { + return number; + } + + public void setNumber(String number) { + this.number = number; + } + + public Comic getComic() { + return comic; + } + + public void setComic(Comic comic) { + this.comic = comic; + } + + public Boolean getIs_read() { + return is_read; + } + + public void setIs_read(Boolean is_read) { + this.is_read = is_read; + } + + public Boolean getIs_stock() { + return is_stock; + } + + public void setIs_stock(Boolean is_stock) { + this.is_stock = is_stock; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/IssueEntity.java b/java/src/main/java/com/ibtp/kontor/comics/entity/IssueEntity.java new file mode 100644 index 0000000..93a7662 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/IssueEntity.java @@ -0,0 +1,75 @@ +package com.ibtp.kontor.comics.entity; + +import javax.persistence.*; + +/** + * Created by thomas on 18.01.15. + */ +@NamedQueries({ + @NamedQuery(name="Issue.findAll", query="SELECT i from IssueEntity as i"), + @NamedQuery(name="Issue.findByNumber", query="SELECT i from IssueEntity as i WHERE i.number = :number") +}) + +@Entity +@Table(name = "ISSUE") +public class IssueEntity { + + private Long id; + + private String number; + + private Boolean completed; + + private ComicEntity comic; + + private ArtistEntity writer; + + private ArtistEntity inker; + + private ArtistEntity penciler; + + private StoryArcEntity storyArc; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getNumber() { return number; } + + public void setNumber(String number) { this.number = number; } + + @Column + public Boolean getCompleted() { return completed; } + public Boolean isCompleted() { return completed; } + + public void setCompleted(Boolean completed) { this.completed = completed; } + + public void setComic(ComicEntity comic) { this.comic = comic; } + + @ManyToOne + public ComicEntity getComic() { return comic; } + + public void setWriter(ArtistEntity writer) { this.writer = writer; } + + @ManyToOne + public ArtistEntity getWriter() { return writer; } + + public void setInker(ArtistEntity inker) { this.inker = inker; } + + @ManyToOne + public ArtistEntity getInker() { return inker; } + + public void setPenciler(ArtistEntity penciler) { this.penciler = penciler; } + + @ManyToOne + public ArtistEntity getPenciler() { return penciler; } + + public void setStoryArc(StoryArcEntity storyArc) { this.storyArc = storyArc; } + + @ManyToOne + public StoryArcEntity getStoryArc() { return storyArc; } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/Publisher.java b/java/src/main/java/com/ibtp/kontor/comics/entity/Publisher.java new file mode 100644 index 0000000..e147d59 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/Publisher.java @@ -0,0 +1,60 @@ +package com.ibtp.kontor.comics.entity; + +import com.ibtp.kontor.Database; +import org.bson.types.ObjectId; +import org.mongodb.morphia.Datastore; +import org.mongodb.morphia.annotations.Entity; +import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Property; + +import java.util.List; + +/** + * Created by thomas on 01.03.16. + */ +@Entity("publisher") +public class Publisher { + + public Publisher() {} + + public Publisher(String name) { + setName(name); + } + + @Id + private ObjectId id; + + @Property + private String name; + + public ObjectId getId() { + return id; + } + + public void setId(ObjectId id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + + public static Publisher getById(ObjectId publisherId) { + Datastore store = Database.init().getDatastore("comics"); + List results = store.createQuery(Publisher.class).field("id").equal(publisherId).asList(); + if (results.size() > 0) { + return results.get(0); + } else { + return null; + } + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/PublisherEntity.java b/java/src/main/java/com/ibtp/kontor/comics/entity/PublisherEntity.java new file mode 100644 index 0000000..9aec40e --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/PublisherEntity.java @@ -0,0 +1,47 @@ +package com.ibtp.kontor.comics.entity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by thomas on 17.01.15. + */ +@NamedQueries({ + @NamedQuery(name="Publisher.findAll", query="SELECT p from PublisherEntity as p"), + @NamedQuery(name="Publisher.findByName", query="SELECT p from PublisherEntity as p WHERE p.name = :name") +}) + +@Entity +@Table(name = "PUBLISHER") +public class PublisherEntity { + + private Long id; + + private String name; + + private Collection comic = new ArrayList(); + + public PublisherEntity() {} + + public PublisherEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy= GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { return name; } + + void setName(String name) { this.name = name; } + + public void setComic(Collection comic) { this.comic = comic; } + + @OneToMany(mappedBy="publisher", cascade=CascadeType.REMOVE) + public Collection getComic() { return comic; } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/StoryArc.java b/java/src/main/java/com/ibtp/kontor/comics/entity/StoryArc.java new file mode 100644 index 0000000..4a4812b --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/StoryArc.java @@ -0,0 +1,59 @@ +package com.ibtp.kontor.comics.entity; + +import org.bson.types.ObjectId; +import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Property; +import org.mongodb.morphia.annotations.Reference; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by thomas on 01.03.16. + */ +public class StoryArc { + + @Id + private ObjectId id; + + @Property + private String name; + + @Reference + private Comic comic; + + @Reference + private List issues = new ArrayList(); + + public ObjectId getId() { + return id; + } + + public void setId(ObjectId id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Comic getComic() { + return comic; + } + + public void setComic(Comic comic) { + this.comic = comic; + } + + public List getIssues() { + return issues; + } + + public void setIssues(List issues) { + this.issues = issues; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/StoryArcEntity.java b/java/src/main/java/com/ibtp/kontor/comics/entity/StoryArcEntity.java new file mode 100644 index 0000000..e3ea22b --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/StoryArcEntity.java @@ -0,0 +1,48 @@ +package com.ibtp.kontor.comics.entity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by thomas on 17.01.15. + */ +@NamedQueries({ + @NamedQuery(name="StoryArc.findAll", query="SELECT s from StoryArcEntity as s"), + @NamedQuery(name="StoryArc.findByTitle", query="SELECT s from StoryArcEntity as s WHERE s.title = :title") +}) + +@Entity +@Table(name = "STORYARC") +public class StoryArcEntity { + + private Long id; + + private String title; + + private Collection issues = new ArrayList(); + + private ComicEntity comic; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + public void setIssues(Collection issues) { this.issues = issues; } + + @OneToMany(mappedBy="storyArc", cascade=CascadeType.REMOVE) + public Collection getIssues() { return issues; } + + public void setComic(ComicEntity comic) { this.comic = comic; } + + @ManyToOne + public ComicEntity getComic() { return comic; } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/TradePaperback.java b/java/src/main/java/com/ibtp/kontor/comics/entity/TradePaperback.java new file mode 100644 index 0000000..161afd7 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/TradePaperback.java @@ -0,0 +1,56 @@ +package com.ibtp.kontor.comics.entity; + + +import org.bson.types.ObjectId; +import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Reference; + +/** + * Created by thomas on 01.03.16. + */ +public class TradePaperback { + + @Id + private ObjectId id; + + @Reference + private Comic comic; + + @Reference + private String issue_start; + + @Reference + private String issue_end; + + public ObjectId getId() { + return id; + } + + public void setId(ObjectId id) { + this.id = id; + } + + public Comic getComic() { + return comic; + } + + public void setComic(Comic comic) { + this.comic = comic; + } + + public String getIssue_start() { + return issue_start; + } + + public void setIssue_start(String issue_start) { + this.issue_start = issue_start; + } + + public String getIssue_end() { + return issue_end; + } + + public void setIssue_end(String issue_end) { + this.issue_end = issue_end; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/Volume.java b/java/src/main/java/com/ibtp/kontor/comics/entity/Volume.java new file mode 100644 index 0000000..7191824 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/Volume.java @@ -0,0 +1,70 @@ +package com.ibtp.kontor.comics.entity; + +import org.bson.types.ObjectId; +import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Property; +import org.mongodb.morphia.annotations.Reference; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by thomas on 01.03.16. + */ +public class Volume { + + public Volume() {} + + public Volume(String name) { + setName(name); + } + + @Id + private ObjectId id; + + @Property + private String name; + + @Reference + private Comic comic; + + @Reference + private List issues = new ArrayList(); + + public ObjectId getId() { + return id; + } + + public void setId(ObjectId id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Comic getComic() { + return comic; + } + + public void setComic(Comic comic) { + this.comic = comic; + } + + public List getIssues() { + return issues; + } + + public void setIssues(List issues) { + this.issues = issues; + } + + @Override + public String toString() { + return name; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/VolumeEntity.java b/java/src/main/java/com/ibtp/kontor/comics/entity/VolumeEntity.java new file mode 100644 index 0000000..6f896ad --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/VolumeEntity.java @@ -0,0 +1,40 @@ +package com.ibtp.kontor.comics.entity; + + +import javax.persistence.*; + +/** + * Created by TPEETZ on 19.01.2015. + */ +@NamedQueries({ + @NamedQuery(name="Volume.findAll", query="SELECT v from VolumeEntity as v"), + @NamedQuery(name="Volume.findByTitle", query="SELECT v from VolumeEntity as v WHERE v.title = :title") +}) + +@Entity +@Table(name = "VOLUME") +public class VolumeEntity { + + private Long id; + + private String title; + + private ComicEntity comic; + + @Id + @GeneratedValue(strategy= GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + @ManyToOne + public ComicEntity getComic() { return comic; } + + public void setComic(ComicEntity comic) { this.comic = comic; } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/view/ComicsMenu.java b/java/src/main/java/com/ibtp/kontor/comics/view/ComicsMenu.java new file mode 100644 index 0000000..5201a11 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/view/ComicsMenu.java @@ -0,0 +1,13 @@ +package com.ibtp.kontor.comics.view; + +import javax.swing.*; + +/** + * Created by tpeetz on 12.02.2015. + */ +public class ComicsMenu extends JMenu { + + public ComicsMenu() { + super("Comics"); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/dal/BaseImpl.java b/java/src/main/java/com/ibtp/kontor/dal/BaseImpl.java new file mode 100644 index 0000000..fa22722 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/dal/BaseImpl.java @@ -0,0 +1,21 @@ +package com.ibtp.kontor.dal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.EntityManager; + +/** + * Created by TPEETZ on 16.01.2015. + */ +public class BaseImpl { + + protected BaseImpl() { + Logger logger = LoggerFactory.getLogger(this.getClass().getName()); + logger.info("BaseImpl started"); + } + + protected EntityManager getEntityManager() { + return DatabaseManager.getDatabase().getEntityManager(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/dal/Database.java b/java/src/main/java/com/ibtp/kontor/dal/Database.java new file mode 100644 index 0000000..67dbda9 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/dal/Database.java @@ -0,0 +1,11 @@ +package com.ibtp.kontor.dal; + +import javax.persistence.EntityManager; + +/** + * Created by TPEETZ on 21.01.2015. + */ +public interface Database { + + public EntityManager getEntityManager(); +} diff --git a/java/src/main/java/com/ibtp/kontor/dal/DatabaseManager.java b/java/src/main/java/com/ibtp/kontor/dal/DatabaseManager.java new file mode 100644 index 0000000..0ccc25d --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/dal/DatabaseManager.java @@ -0,0 +1,23 @@ +package com.ibtp.kontor.dal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Created by TPEETZ on 22.01.2015. + */ +public class DatabaseManager { + + private static Database database; + private static Logger logger = LoggerFactory.getLogger(DatabaseManager.class.getName()); + + public static Database getDatabase() { + logger.info("return " + database.toString()); + return database; + } + + public static void setDatabase(Database database) { + logger.info("set " + database.toString()); + DatabaseManager.database = database; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/dal/LocalDatabase.java b/java/src/main/java/com/ibtp/kontor/dal/LocalDatabase.java new file mode 100644 index 0000000..e0ec9c1 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/dal/LocalDatabase.java @@ -0,0 +1,104 @@ +package com.ibtp.kontor.dal; + +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hsqldb.Server; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import javax.persistence.spi.PersistenceProvider; +import javax.persistence.spi.PersistenceProviderResolver; +import javax.persistence.spi.PersistenceProviderResolverHolder; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +public class LocalDatabase implements Database { + + private static Server server; + private static EntityManagerFactory factory; + private static EntityManager em; + private static Logger logger = LoggerFactory.getLogger(LocalDatabase.class.getName()); + + private LocalDatabase() { + logger.info("LocalDatabase started"); + } + + private static void assureDatabaseRunning() { + if (LocalDatabase.server == null) { + LocalDatabase.startDatabase(); + } + } + + private static void startDatabase() { + Logger logger = LoggerFactory.getLogger(LocalDatabase.class.getName()); + logger.info("startDatabase as kontor in hsqldb_databases/kontor"); + LocalDatabase.server = new Server(); + LocalDatabase.server.setAddress("localhost"); + LocalDatabase.server.setDatabaseName(0, "kontor"); + LocalDatabase.server.setDatabasePath(0, "file:hsqldb_databases/kontor"); + LocalDatabase.server.setPort(2345); + LocalDatabase.server.setTrace(true); + LocalDatabase.server.setLogWriter(new PrintWriter(System.out)); + LocalDatabase.server.start(); + } + + private static void stopDatabase() { + server.shutdown(); + } + + private static EntityManagerFactory getFactory() { + if (LocalDatabase.factory == null) { + LocalDatabase.assureDatabaseRunning(); + PersistenceProviderResolverHolder.setPersistenceProviderResolver(new PersistenceProviderResolver() { + private final List providers_ = Arrays.asList((PersistenceProvider) new HibernatePersistenceProvider()); + + @Override + public void clearCachedProviders() { + // Auto-generated method stub + } + + @Override + public List getPersistenceProviders() { + return providers_; + } + }); + LocalDatabase.factory = Persistence.createEntityManagerFactory("com.ibtp.kontor"); + logger.info("EntityManagerFactory(com.ibtp.kontor) created"); + } + return LocalDatabase.factory; + } + + private static EntityManager getSingleEntityManager() { + return LocalDatabase.em; + } + + private static void setSingleEntityManager(EntityManager manager) { + LocalDatabase.em = manager; + } + + @Override + public EntityManager getEntityManager() { + if (getSingleEntityManager() == null) { + setSingleEntityManager(getFactory().createEntityManager()); + logger.info("EntityManager created"); + } + return getSingleEntityManager(); + } + + @Override + public String toString() { + String serverMessage; + if (LocalDatabase.server == null) { + serverMessage = "server:null"; + } else { + serverMessage = LocalDatabase.server.toString(); + } + return LocalDatabase.class.getName() + " " + serverMessage; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/dal/ArticleDao.java b/java/src/main/java/com/ibtp/kontor/library/dal/ArticleDao.java new file mode 100644 index 0000000..f9cd144 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/dal/ArticleDao.java @@ -0,0 +1,23 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.library.entity.ArticleEntity; + +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +interface ArticleDao { + + public ArticleEntity getById(Long id); + + public List findByIds(List ids); + + public List findAll(); + + public List findByTitle(String title); + + public ArticleEntity store(ArticleEntity entity); + + public void delete(ArticleEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/library/dal/ArticleImpl.java b/java/src/main/java/com/ibtp/kontor/library/dal/ArticleImpl.java new file mode 100644 index 0000000..ada92d9 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/dal/ArticleImpl.java @@ -0,0 +1,64 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.library.entity.ArticleEntity; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +public class ArticleImpl extends BaseImpl implements ArticleDao { + + @Override + public ArticleEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Article.findById"); + query.setParameter("id", id); + return (ArticleEntity)query.getSingleResult(); + } + + @Override + public List findByIds(List ids) { + return null; + } + + @Override + public List findAll() { + Query query = getEntityManager().createNamedQuery("Article.findAll"); + //noinspection unchecked + return query.getResultList(); + } + + @Override + public List findByTitle(String title) { + Query query = getEntityManager().createNamedQuery("Article.findByTitle"); + query.setParameter("title", title); + //noinspection unchecked + return query.getResultList(); + } + + public ArticleEntity addArticle(String title) { + ArticleEntity entity = new ArticleEntity(title); + store(entity); + return entity; + } + + @Override + public ArticleEntity store(ArticleEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(ArticleEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/dal/AuthorDao.java b/java/src/main/java/com/ibtp/kontor/library/dal/AuthorDao.java new file mode 100644 index 0000000..85f4c83 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/dal/AuthorDao.java @@ -0,0 +1,25 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.library.entity.AuthorEntity; + +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +interface AuthorDao { + + public AuthorEntity getById(Long id); + + public List findByIds(List ids); + + public List findByName(String name); + + public List findAll(); + + public AuthorEntity addAuthor(String name); + + public AuthorEntity store(AuthorEntity entity); + + public void delete(AuthorEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/library/dal/AuthorImpl.java b/java/src/main/java/com/ibtp/kontor/library/dal/AuthorImpl.java new file mode 100644 index 0000000..573d7d9 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/dal/AuthorImpl.java @@ -0,0 +1,65 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.library.entity.AuthorEntity; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.List; + +/** + * Created by thomas on 23.01.15. + */ +public class AuthorImpl extends BaseImpl implements AuthorDao { + + public AuthorImpl() {} + + @Override + public AuthorEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Author.findById"); + query.setParameter("id", id); + return (AuthorEntity)query.getSingleResult(); + } + + @Override + public List findByIds(List ids) { + return null; + } + + @Override + public List findByName(String name) { + Query query = getEntityManager().createNamedQuery("Author.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public List findAll() { + Query query = getEntityManager().createNamedQuery("Author.findAll"); + return query.getResultList(); + } + + @Override + public AuthorEntity addAuthor(String name) { + AuthorEntity author = new AuthorEntity(name); + store(author); + return author; + } + + @Override + public AuthorEntity store(AuthorEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(AuthorEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/dal/BookDao.java b/java/src/main/java/com/ibtp/kontor/library/dal/BookDao.java new file mode 100644 index 0000000..d89a1b7 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/dal/BookDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.library.entity.BookEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +interface BookDao { + + public BookEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public BookEntity store(BookEntity entity); + + public void delete(BookEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/library/dal/BookImpl.java b/java/src/main/java/com/ibtp/kontor/library/dal/BookImpl.java new file mode 100644 index 0000000..63f0ab1 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/dal/BookImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.library.entity.BookEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class BookImpl extends BaseImpl implements BookDao { + + @Override + public BookEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public BookEntity store(BookEntity entity) { + return null; + } + + @Override + public void delete(BookEntity entity) { + + } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/dal/FileDao.java b/java/src/main/java/com/ibtp/kontor/library/dal/FileDao.java new file mode 100644 index 0000000..f123e69 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/dal/FileDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.library.entity.FileEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +interface FileDao { + + public FileEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public FileEntity store(FileEntity entity); + + public void delete(FileEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/library/dal/FileImpl.java b/java/src/main/java/com/ibtp/kontor/library/dal/FileImpl.java new file mode 100644 index 0000000..1af2900 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/dal/FileImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.library.entity.FileEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class FileImpl extends BaseImpl implements FileDao { + + @Override + public FileEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public FileEntity store(FileEntity entity) { + return null; + } + + @Override + public void delete(FileEntity entity) { + + } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/dal/TitleDao.java b/java/src/main/java/com/ibtp/kontor/library/dal/TitleDao.java new file mode 100644 index 0000000..81da92b --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/dal/TitleDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.library.entity.TitleEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +interface TitleDao { + + public TitleEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public TitleEntity store(TitleEntity entity); + + public void delete(TitleEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/library/dal/TitleImpl.java b/java/src/main/java/com/ibtp/kontor/library/dal/TitleImpl.java new file mode 100644 index 0000000..410c2c5 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/dal/TitleImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.library.entity.TitleEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class TitleImpl extends BaseImpl implements TitleDao { + + @Override + public TitleEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public TitleEntity store(TitleEntity entity) { + return null; + } + + @Override + public void delete(TitleEntity entity) { + + } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/entity/ArticleEntity.java b/java/src/main/java/com/ibtp/kontor/library/entity/ArticleEntity.java new file mode 100644 index 0000000..d61eda5 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/entity/ArticleEntity.java @@ -0,0 +1,56 @@ +package com.ibtp.kontor.library.entity; + +import javax.persistence.*; + +/** + * Created by TPEETZ on 23.01.2015. + */ +@NamedQueries({ + @NamedQuery(name="Article.findAll", query="SELECT a from ArticleEntity as a"), + @NamedQuery(name="Article.findByTitle", query="SELECT a from ArticleEntity as a WHERE a.title = :title") +}) + +@Entity +@Table(name = "ARTICLE") +public class ArticleEntity { + + private Long id; + + private String title; + + private AuthorEntity author; + + public ArticleEntity() {} + + public ArticleEntity(String title) { + setTitle(title); + } + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @Column + public String getTitle() { + return title; + } + + void setTitle(String title) { + this.title = title; + } + + @ManyToOne + public AuthorEntity getAuthor() { + return author; + } + + public void setAuthor(AuthorEntity author) { + this.author = author; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/entity/AuthorEntity.java b/java/src/main/java/com/ibtp/kontor/library/entity/AuthorEntity.java new file mode 100644 index 0000000..63a6f4b --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/entity/AuthorEntity.java @@ -0,0 +1,62 @@ +package com.ibtp.kontor.library.entity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by TPEETZ on 23.01.2015. + */ +@NamedQueries({ + @NamedQuery(name="Author.findAll", query="SELECT a from AuthorEntity as a"), + @NamedQuery(name="Author.findById", query="SELECT a from AuthorEntity as a WHERE a.id = :id"), + @NamedQuery(name="Author.findByName", query="SELECT a from AuthorEntity as a WHERE a.name = :name") +}) +@Entity +@Table(name="AUTHOR") +public class AuthorEntity { + + private Long id; + + private String name; + + private Collection books = new ArrayList(); + + private Collection articles = new ArrayList(); + + public AuthorEntity() {} + + public AuthorEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy= GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { return name; } + + void setName(String name) { this.name = name; } + + @OneToMany(mappedBy="author", cascade=CascadeType.REMOVE) + public Collection getBooks() { + return books; + } + + public void setBooks(Collection books) { + this.books = books; + } + + @OneToMany(mappedBy="author", cascade=CascadeType.REMOVE) + public Collection getArticles() { + return articles; + } + + public void setArticles(Collection articles) { + this.articles = articles; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/entity/BookEntity.java b/java/src/main/java/com/ibtp/kontor/library/entity/BookEntity.java new file mode 100644 index 0000000..b57705c --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/entity/BookEntity.java @@ -0,0 +1,96 @@ +package com.ibtp.kontor.library.entity; + +import javax.persistence.*; + +/** + * Created by TPEETZ on 23.01.2015. + */ +@Entity +@Table(name = "BOOK") +public class BookEntity { + + private Long id; + + private String title; + + private AuthorEntity author; + + private String publisher; + + private String isbn; + + private Long page; + + private String edition; + + public BookEntity() {} + + public BookEntity(String title) { + setTitle(title); + } + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + public Long getId() { + return id; + } + + /* unused */ + public void setId(Long id) { + this.id = id; + } + + @Column + public String getTitle() { + return title; + } + + void setTitle(String title) { + this.title = title; + } + + @ManyToOne + public AuthorEntity getAuthor() { + return author; + } + + public void setAuthor(AuthorEntity author) { + this.author = author; + } + + @Column + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + @Column + public Long getPage() { + return page; + } + + public void setPage(Long page) { + this.page = page; + } + + @Column + public String getEdition() { + return edition; + } + + public void setEdition(String edition) { + this.edition = edition; + } + + @Column + public String getPublisher() { + return publisher; + } + + public void setPublisher(String publisher) { + this.publisher = publisher; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/entity/FileEntity.java b/java/src/main/java/com/ibtp/kontor/library/entity/FileEntity.java new file mode 100644 index 0000000..05f4577 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/entity/FileEntity.java @@ -0,0 +1,41 @@ +package com.ibtp.kontor.library.entity; + +import javax.persistence.*; + +/** + * Created by TPEETZ on 23.01.2015. + */ +@Entity +@Table(name = "FILE") +public class FileEntity { + + private Long id; + + private String title; + + public FileEntity() {} + + public FileEntity(String title) { + setTitle(title); + } + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + public Long getId() { + return id; + } + + /* unused */ + public void setId(Long id) { + this.id = id; + } + + @Column + public String getTitle() { + return title; + } + + void setTitle(String title) { + this.title = title; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/entity/TitleEntity.java b/java/src/main/java/com/ibtp/kontor/library/entity/TitleEntity.java new file mode 100644 index 0000000..ba25eb9 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/entity/TitleEntity.java @@ -0,0 +1,27 @@ +package com.ibtp.kontor.library.entity; + +import javax.persistence.*; + +/** + * Created by TPEETZ on 23.01.2015. + */ +@Entity +@Table(name = "TITLE") +public class TitleEntity { + + private Long id; + + private String title; + + @Id + @GeneratedValue(strategy= GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/view/LibraryMenu.java b/java/src/main/java/com/ibtp/kontor/library/view/LibraryMenu.java new file mode 100644 index 0000000..0b97e27 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/view/LibraryMenu.java @@ -0,0 +1,13 @@ +package com.ibtp.kontor.library.view; + +import javax.swing.*; + +/** + * Created by tpeetz on 12.02.2015. + */ +public class LibraryMenu extends JMenu { + + public LibraryMenu() { + super("Library"); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetDao.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetDao.java new file mode 100644 index 0000000..29692d4 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.BaseSetEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface BaseSetDao { + + public BaseSetEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public BaseSetEntity store(BaseSetEntity entity); + + public void delete(BaseSetEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetImpl.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetImpl.java new file mode 100644 index 0000000..9e584a4 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetImpl.java @@ -0,0 +1,51 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.BaseSetEntity; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class BaseSetImpl extends BaseImpl implements BaseSetDao { + + @Override + public BaseSetEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("BaseSet.findById"); + query.setParameter("id", id); + return (BaseSetEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + Query query = getEntityManager().createNamedQuery("BaseSet.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public BaseSetEntity store(BaseSetEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(BaseSetEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertDao.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertDao.java new file mode 100644 index 0000000..c68790f --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.InsertEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface InsertDao { + + public InsertEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public InsertEntity store(InsertEntity entity); + + public void delete(InsertEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertImpl.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertImpl.java new file mode 100644 index 0000000..598763c --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.InsertEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class InsertImpl extends BaseImpl implements InsertDao { + + @Override + public InsertEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public InsertEntity store(InsertEntity entity) { + return null; + } + + @Override + public void delete(InsertEntity entity) { + + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerDao.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerDao.java new file mode 100644 index 0000000..451d18b --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerDao.java @@ -0,0 +1,29 @@ +package com.ibtp.kontor.tradingcards.dal; + + +import com.ibtp.kontor.tradingcards.entity.BaseSetEntity; +import com.ibtp.kontor.tradingcards.entity.ManufacturerEntity; + +import java.util.Collection; +import java.util.List; + +/** + * + * @author tpeetz + */ +interface ManufacturerDao { + + public ManufacturerEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public ManufacturerEntity assignBaseSet(ManufacturerEntity manufacturer, BaseSetEntity baseSet); + + public ManufacturerEntity addManufacturer(String name); + + public ManufacturerEntity store(ManufacturerEntity entity); + + public void delete(ManufacturerEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImpl.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImpl.java new file mode 100644 index 0000000..58c2fd6 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImpl.java @@ -0,0 +1,65 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.BaseSetEntity; +import com.ibtp.kontor.tradingcards.entity.ManufacturerEntity; + +import java.util.List; +import javax.persistence.EntityManager; +import javax.persistence.Query; + +/** + * + * @author tpeetz + */ +public class ManufacturerImpl extends BaseImpl implements ManufacturerDao { + + @Override + public ManufacturerEntity getById(Long id) { + Query q = getEntityManager().createNamedQuery("Manufacturer.findById"); + q.setParameter("id", id); + return (ManufacturerEntity)q.getSingleResult(); + } + + @Override + public List findByIds(List ids) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @SuppressWarnings("unchecked") + @Override + public List findByName(String name) { + Query q = getEntityManager().createNamedQuery("Manufacturer.findByName"); + q.setParameter("name", name); + return q.getResultList(); + } + + @Override + public ManufacturerEntity assignBaseSet(ManufacturerEntity comic, BaseSetEntity baseSet) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public ManufacturerEntity addManufacturer(String name) { + ManufacturerEntity manufacturer = new ManufacturerEntity(name); + store(manufacturer); + return manufacturer; + } + + @Override + public ManufacturerEntity store(ManufacturerEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(ManufacturerEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetDao.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetDao.java new file mode 100644 index 0000000..c4d36ba --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.ParallelSetEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface ParallelSetDao { + + public ParallelSetEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public ParallelSetEntity store(ParallelSetEntity entity); + + public void delete(ParallelSetEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImpl.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImpl.java new file mode 100644 index 0000000..cb604a0 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.ParallelSetEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class ParallelSetImpl extends BaseImpl implements ParallelSetDao { + + @Override + public ParallelSetEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public ParallelSetEntity store(ParallelSetEntity entity) { + return null; + } + + @Override + public void delete(ParallelSetEntity entity) { + + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerDao.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerDao.java new file mode 100644 index 0000000..11d045e --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerDao.java @@ -0,0 +1,24 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.PlayerEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface PlayerDao { + + public PlayerEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public PlayerEntity addPlayer(String name); + + public PlayerEntity store(PlayerEntity entity); + + public void delete(PlayerEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerImpl.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerImpl.java new file mode 100644 index 0000000..c3d5104 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerImpl.java @@ -0,0 +1,43 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.PlayerEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class PlayerImpl extends BaseImpl implements PlayerDao { + + @Override + public PlayerEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public PlayerEntity addPlayer(String name) { + return null; + } + + @Override + public PlayerEntity store(PlayerEntity entity) { + return null; + } + + @Override + public void delete(PlayerEntity entity) { + + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionDao.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionDao.java new file mode 100644 index 0000000..9970491 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.PositionEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface PositionDao { + + public PositionEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public Collection findAll(); + + public PositionEntity addPosition(String name); + + public PositionEntity store(PositionEntity entity); + + public void delete(PositionEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionImpl.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionImpl.java new file mode 100644 index 0000000..e2a22ae --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionImpl.java @@ -0,0 +1,65 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.PositionEntity; + +import java.util.Collection; +import java.util.List; + +import javax.persistence.EntityManager; +import javax.persistence.Query; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class PositionImpl extends BaseImpl implements PositionDao { + + @Override + public PositionEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Position.findById"); + query.setParameter("id", id); + return (PositionEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + Query query = getEntityManager().createNamedQuery("Position.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Position.findAll"); + return query.getResultList(); + } + + @Override + public PositionEntity addPosition(String name) { + PositionEntity position = new PositionEntity(name); + store(position); + return position; + } + + @Override + public PositionEntity store(PositionEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(PositionEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardDao.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardDao.java new file mode 100644 index 0000000..25fb244 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.SportCardEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface SportCardDao { + + public SportCardEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public SportCardEntity store(SportCardEntity entity); + + public void delete(SportCardEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardImpl.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardImpl.java new file mode 100644 index 0000000..5939d2c --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.SportCardEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class SportCardImpl extends BaseImpl implements SportCardDao { + + @Override + public SportCardEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public SportCardEntity store(SportCardEntity entity) { + return null; + } + + @Override + public void delete(SportCardEntity entity) { + + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportDao.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportDao.java new file mode 100644 index 0000000..f5ba51e --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.SportEntity; + +import java.util.Collection; +import java.util.List; + +/** + * + * @author tpeetz + */ +interface SportDao { + public SportEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public Collection findAll(); + + public SportEntity addSport(String name); + + public SportEntity store(SportEntity entity); + + public void delete(SportEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportImpl.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportImpl.java new file mode 100644 index 0000000..223236d --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportImpl.java @@ -0,0 +1,64 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.SportEntity; + +import java.util.Collection; +import java.util.List; +import javax.persistence.EntityManager; +import javax.persistence.Query; + +/** + * + * @author tpeetz + */ +public class SportImpl extends BaseImpl implements SportDao { + + @Override + public SportEntity getById(Long id) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public List findByIds(List ids) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @SuppressWarnings("unchecked") + @Override + public List findByName(String name) { + Query query = getEntityManager().createNamedQuery("Sport.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Sport.findAll"); + return query.getResultList(); + } + + @Override + public SportEntity addSport(String name) { + SportEntity sport = new SportEntity(name); + store(sport); + return sport; + } + + @Override + public SportEntity store(SportEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(SportEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamDao.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamDao.java new file mode 100644 index 0000000..c2f9eff --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.SportEntity; +import com.ibtp.kontor.tradingcards.entity.TeamEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +interface TeamDao { + public TeamEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public Collection findAll(); + + public TeamEntity addTeam(String name, SportEntity sport); + + public TeamEntity store(TeamEntity entity); + + public void delete(TeamEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamImpl.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamImpl.java new file mode 100644 index 0000000..7fb8244 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamImpl.java @@ -0,0 +1,66 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.SportEntity; +import com.ibtp.kontor.tradingcards.entity.TeamEntity; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +public class TeamImpl extends BaseImpl implements TeamDao { + + @Override + public TeamEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Team.findById"); + query.setParameter("id", id); + return (TeamEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + Query query = getEntityManager().createNamedQuery("Team.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Team.findAll"); + return query.getResultList(); + } + + @Override + public TeamEntity addTeam(String name, SportEntity sport) { + TeamEntity team = new TeamEntity(name); + team.setSport(sport); + store(team); + return team; + } + + @Override + public TeamEntity store(TeamEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(TeamEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/entity/BaseSetEntity.java b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/BaseSetEntity.java new file mode 100644 index 0000000..3111e98 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/BaseSetEntity.java @@ -0,0 +1,67 @@ +package com.ibtp.kontor.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="BASESET") +public class BaseSetEntity { + private Long id; + private String name; + private ManufacturerEntity manufacturer; + private Collection parallelSets = new ArrayList(); + private Collection inserts = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ManyToOne + public ManufacturerEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(ManufacturerEntity manufacturer) { + this.manufacturer = manufacturer; + } + + @OneToMany(mappedBy="baseSet", cascade=CascadeType.REMOVE) + public Collection getParallelSets() { + return parallelSets; + } + + public void setParallelSets(Collection parallelSets) { + this.parallelSets = parallelSets; + } + + @OneToMany(mappedBy="baseSet", cascade=CascadeType.REMOVE) + public Collection getInserts() { + return inserts; + } + + public void setInserts(Collection inserts) { + this.inserts = inserts; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/entity/InsertEntity.java b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/InsertEntity.java new file mode 100644 index 0000000..ce59037 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/InsertEntity.java @@ -0,0 +1,74 @@ +package com.ibtp.kontor.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="InsertSet.findAll", query="SELECT i from InsertEntity as i"), + @NamedQuery(name="InsertSet.findById", query="SELECT i from InsertEntity as i WHERE i.id = :id"), + @NamedQuery(name="InsertSet.findByName", query="SELECT i from InsertEntity as i WHERE i.name = :name") +}) + +@Entity +@Table(name="INSERTSET") +public class InsertEntity { + private Long id; + private String name; + private ManufacturerEntity manufacturer; + private BaseSetEntity baseSet; + private Collection sportCard = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ManyToOne + public ManufacturerEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(ManufacturerEntity manufacturer) { + this.manufacturer = manufacturer; + } + + @ManyToOne + public BaseSetEntity getBaseSet() { + return baseSet; + } + + public void setBaseSet(BaseSetEntity baseSet) { + this.baseSet = baseSet; + } + + @OneToMany(mappedBy="insert", cascade=CascadeType.REMOVE) + public Collection getSportCard() { + return sportCard; + } + + public void setSportCard(Collection sportCard) { + this.sportCard = sportCard; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/entity/ManufacturerEntity.java b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/ManufacturerEntity.java new file mode 100644 index 0000000..f83da2a --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/ManufacturerEntity.java @@ -0,0 +1,78 @@ +package com.ibtp.kontor.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Manufacturer.findAll", query="SELECT m from ManufacturerEntity as m"), + @NamedQuery(name="Manufacturer.findByName", query="SELECT m from ManufacturerEntity as m WHERE m.name = :name") +}) + +@Entity +@Table(name="MANUFACTURER") +public class ManufacturerEntity { + + private Long id; + private String name; + private Collection baseSets = new ArrayList(); + private Collection parallelSets = new ArrayList(); + private Collection inserts = new ArrayList(); + + public ManufacturerEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + void setName(String name) { + this.name = name; + } + + @OneToMany(mappedBy="manufacturer", cascade=CascadeType.REMOVE) + public Collection getBaseSets() { + return baseSets; + } + + public void setBaseSets(Collection baseSets) { + this.baseSets = baseSets; + } + + @OneToMany(mappedBy="manufacturer", cascade=CascadeType.REMOVE) + public Collection getParallelSets() { + return parallelSets; + } + + public void setParallelSets(Collection parallelSets) { + this.parallelSets = parallelSets; + } + + @OneToMany(mappedBy="manufacturer", cascade=CascadeType.REMOVE) + public Collection getInserts() { + return inserts; + } + + public void setInserts(Collection inserts) { + this.inserts = inserts; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/entity/ParallelSetEntity.java b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/ParallelSetEntity.java new file mode 100644 index 0000000..0ce0c69 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/ParallelSetEntity.java @@ -0,0 +1,53 @@ +package com.ibtp.kontor.tradingcards.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name="PARALLELSET") +public class ParallelSetEntity { + private Long id; + private String name; + private ManufacturerEntity manufacturer; + + private BaseSetEntity baseSet; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + @ManyToOne + public ManufacturerEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(ManufacturerEntity manufacturer) { + this.manufacturer = manufacturer; + } + + public void setName(String name) { + this.name = name; + } + + @ManyToOne + public BaseSetEntity getBaseSet() { + return baseSet; + } + + public void setBaseSet(BaseSetEntity baseSet) { + this.baseSet = baseSet; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/entity/PlayerEntity.java b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/PlayerEntity.java new file mode 100644 index 0000000..7c19d70 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/PlayerEntity.java @@ -0,0 +1,46 @@ +package com.ibtp.kontor.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="PLAYER") +public class PlayerEntity { + private Long id; + private TeamEntity team; + private Collection cards = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @ManyToOne + public TeamEntity getTeam() { + return team; + } + + public void setTeam(TeamEntity team) { + this.team = team; + } + + @OneToMany(mappedBy="player", cascade=CascadeType.REMOVE) + public Collection getCards() { + return cards; + } + + public void setCards(Collection cards) { + this.cards = cards; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/entity/PositionEntity.java b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/PositionEntity.java new file mode 100644 index 0000000..0e5c6b8 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/PositionEntity.java @@ -0,0 +1,66 @@ +package com.ibtp.kontor.tradingcards.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQuery; +import javax.persistence.NamedQueries; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Position.findAll", query="SELECT p from PositionEntity as p"), + @NamedQuery(name="Position.findByName", query="SELECT p from PositionEntity as p WHERE p.name = :name") +}) + +@Entity +@Table(name="POSITION") +public class PositionEntity { + + private Long id; + private String name; + private String shortName; + private SportEntity sport; + + public PositionEntity() {} + + public PositionEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Column + public String getShortName() { + return shortName; + } + + public void setShortName(String shortName) { + this.shortName = shortName; + } + + @ManyToOne + public SportEntity getSport() { + return sport; + } + + public void setSport(SportEntity sport) { + this.sport = sport; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/entity/SportCardEntity.java b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/SportCardEntity.java new file mode 100644 index 0000000..f71f722 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/SportCardEntity.java @@ -0,0 +1,68 @@ +package com.ibtp.kontor.tradingcards.entity; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="SportCard.findAll", query="SELECT s from SportCardEntity as s") +}) + +@Entity +@Table(name = "SPORTCARD") +public class SportCardEntity { + private Long id; + private PlayerEntity player; + private BaseSetEntity baseSet; + private ParallelSetEntity parallelSet; + private InsertEntity insert; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @ManyToOne + public PlayerEntity getPlayer() { + return player; + } + + public void setPlayer(PlayerEntity player) { + this.player = player; + } + + @ManyToOne + public BaseSetEntity getBaseSet() { + return baseSet; + } + + public void setBaseSet(BaseSetEntity baseSet) { + this.baseSet = baseSet; + } + + @ManyToOne + public ParallelSetEntity getParallelSet() { + return parallelSet; + } + + public void setParallelSet(ParallelSetEntity parallelSet) { + this.parallelSet = parallelSet; + } + + @ManyToOne + public InsertEntity getInsert() { + return insert; + } + + public void setInsert(InsertEntity insert) { + this.insert = insert; + } + +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/entity/SportEntity.java b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/SportEntity.java new file mode 100644 index 0000000..8e2258e --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/SportEntity.java @@ -0,0 +1,75 @@ +package com.ibtp.kontor.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Sport.findAll", query="SELECT s from SportEntity as s"), + @NamedQuery(name="Sport.findByName", query="SELECT s from SportEntity as s WHERE s.name = :name") +}) + +@Entity +@Table(name="SPORT") +public class SportEntity { + + private Long id; + private String name; + private Collection teams = new ArrayList(); + private Collection positions = new ArrayList(); + + public SportEntity() {} + + public SportEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + String getName() { + return name; + } + + void setName(String name) { + this.name = name; + } + + @OneToMany(mappedBy="sport", cascade=CascadeType.REMOVE) + public Collection getTeams() { + return teams; + } + + public void setTeams(Collection teams) { + this.teams = teams; + } + + @OneToMany(mappedBy="sport", cascade=CascadeType.REMOVE) + public Collection getPositions() { + return positions; + } + + public void setPositions(Collection positions) { + this.positions = positions; + } + + @Override + public String toString() { + return "Sport[" + "id=" + getId() + ",name=" + getName() + "]"; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/entity/TeamEntity.java b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/TeamEntity.java new file mode 100644 index 0000000..8dbcb77 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/TeamEntity.java @@ -0,0 +1,48 @@ +package com.ibtp.kontor.tradingcards.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Team.findAll", query="SELECT t from TeamEntity as t"), + @NamedQuery(name="Team.findByName", query="SELECT t from TeamEntity as t WHERE t.name = :name") +}) + +@Entity +@Table(name="TEAM") +public class TeamEntity { + + private Long id; + private String name; + private SportEntity sport; + + public TeamEntity() {} + + public TeamEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { return name; } + + void setName(String name) { this.name = name; } + + @ManyToOne + public SportEntity getSport() { return sport; } + + public void setSport(SportEntity sport) { this.sport = sport; } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/view/TradingCardsMenu.java b/java/src/main/java/com/ibtp/kontor/tradingcards/view/TradingCardsMenu.java new file mode 100644 index 0000000..f124056 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/view/TradingCardsMenu.java @@ -0,0 +1,13 @@ +package com.ibtp.kontor.tradingcards.view; + +import javax.swing.*; + +/** + * Created by TPEETZ on 13.02.2015. + */ +public class TradingCardsMenu extends JMenu { + + public TradingCardsMenu() { + super("TradingCards"); + } +} diff --git a/java/src/main/resources/META-INF/persistence.xml b/java/src/main/resources/META-INF/persistence.xml new file mode 100644 index 0000000..35d671b --- /dev/null +++ b/java/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,39 @@ + + + org.hibernate.jpa.HibernatePersistenceProvider + com.ibtp.kontor.comics.entity.ArtistEntity + com.ibtp.kontor.comics.entity.ComicEntity + com.ibtp.kontor.comics.entity.IssueEntity + com.ibtp.kontor.comics.entity.StoryArcEntity + com.ibtp.kontor.comics.entity.VolumeEntity + com.ibtp.kontor.comics.entity.PublisherEntity + com.ibtp.kontor.library.entity.AuthorEntity + com.ibtp.kontor.library.entity.ArticleEntity + com.ibtp.kontor.library.entity.BookEntity + com.ibtp.kontor.library.entity.FileEntity + com.ibtp.kontor.library.entity.TitleEntity + com.ibtp.kontor.tradingcards.entity.SportEntity + com.ibtp.kontor.tradingcards.entity.TeamEntity + com.ibtp.kontor.tradingcards.entity.PositionEntity + com.ibtp.kontor.tradingcards.entity.PlayerEntity + com.ibtp.kontor.tradingcards.entity.ManufacturerEntity + com.ibtp.kontor.tradingcards.entity.BaseSetEntity + com.ibtp.kontor.tradingcards.entity.InsertEntity + com.ibtp.kontor.tradingcards.entity.ParallelSetEntity + com.ibtp.kontor.tradingcards.entity.SportCardEntity + + + + + + + + + + + + + diff --git a/java/src/main/resources/logback.xml b/java/src/main/resources/logback.xml new file mode 100644 index 0000000..e8cff02 --- /dev/null +++ b/java/src/main/resources/logback.xml @@ -0,0 +1,40 @@ + + + + + + %d{yyyy-MM-dd_HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + + + + + c:/kontor.log + + %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + c:/kontor.%i.log.zip + 1 + 10 + + + + 2MB + + + + + + + + + + + + + diff --git a/java/src/test/java/com/ibtp/kontor/comics/CollectionTest.java b/java/src/test/java/com/ibtp/kontor/comics/CollectionTest.java new file mode 100644 index 0000000..57d01a3 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/comics/CollectionTest.java @@ -0,0 +1,43 @@ +package com.ibtp.kontor.comics; + +import com.ibtp.kontor.comics.dal.PublisherImpl; +import com.ibtp.kontor.comics.entity.PublisherEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collection; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class CollectionTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @AfterAll + public static void cleanup() { + PublisherImpl publisherImpl = new PublisherImpl(); + Collection publisherEntities = publisherImpl.findAll(); + for (PublisherEntity publisherEntity : publisherEntities) { + publisherImpl.delete(publisherEntity); + } + } + + @Test + public void testAddPublishers() { + String publisherName = "Bongo Comics"; + PublisherImpl publisherImpl = new PublisherImpl(); + publisherImpl.addPublisher(publisherName); + publisherImpl.addPublisher("Marvel"); + Collection publisherList = publisherImpl.findAll(); + assertEquals(2, publisherList.size()); + } +} diff --git a/java/src/test/java/com/ibtp/kontor/comics/dal/ArtistImplTest.java b/java/src/test/java/com/ibtp/kontor/comics/dal/ArtistImplTest.java new file mode 100644 index 0000000..e78e77f --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/comics/dal/ArtistImplTest.java @@ -0,0 +1,65 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ArtistEntity; +import com.ibtp.kontor.dal.*; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collection; + +public class ArtistImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + tearDown(); + } + + @AfterAll + public static void tearDown() { + ArtistImpl artistImpl = new ArtistImpl(); + Collection artistList = artistImpl.findAll(); + for (ArtistEntity artistEntity : artistList) { + artistImpl.delete(artistEntity); + } + } + + @Test + public void testArtistAddAndDelete() { + String artistName = "testArtistAddAndDelete"; + ArtistImpl artistImpl = new ArtistImpl(); + artistImpl.addArtist(artistName); + Collection resultList = artistImpl.findByName(artistName); + assertNotNull(resultList); + assertTrue(resultList.size() > 0); + ArtistEntity artist = (ArtistEntity)(resultList.toArray()[0]); + artistImpl.delete(artist); + resultList = artistImpl.findByName(artistName); + assertNotNull(resultList); + assertEquals(0, resultList.size()); + } + + @Test + public void testArtistFindAll() { + ArtistImpl artistImpl = new ArtistImpl(); + Collection artistList = artistImpl.findAll(); + assertNotNull(artistList); + assertEquals(0, artistList.size()); + artistImpl.addArtist("testArtistFindAll1"); + artistImpl.addArtist("testArtistFindAll2"); + artistImpl.addArtist("testArtistFindAll3"); + artistList = artistImpl.findAll(); + assertNotNull(artistList); + assertEquals(3, artistList.size()); + for (ArtistEntity artistEntity : artistList) { + artistImpl.delete(artistEntity); + } + } +} diff --git a/java/src/test/java/com/ibtp/kontor/comics/dal/ComicImplTest.java b/java/src/test/java/com/ibtp/kontor/comics/dal/ComicImplTest.java new file mode 100644 index 0000000..76f2d00 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/comics/dal/ComicImplTest.java @@ -0,0 +1,57 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ComicEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class ComicImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testComicAddAndDelete() { + String comicTitle = "Comic1"; + ComicImpl comicImpl = new ComicImpl(); + comicImpl.addComic(comicTitle); + Collection comicList = comicImpl.findByTitle(comicTitle); + assertNotNull(comicList); + assertEquals(1, comicList.size()); + comicImpl.delete((ComicEntity) comicList.toArray()[0]); + comicList = comicImpl.findByTitle(comicTitle); + assertNotNull(comicList); + assertEquals(0, comicList.size()); + } + + @Test + public void testComicFindAll() { + ComicImpl comicImpl = new ComicImpl(); + comicImpl.addComic("Comic1"); + comicImpl.addComic("Comic2"); + comicImpl.addComic("Comic3"); + Collection comicList = comicImpl.findAll(); + assertNotNull(comicList); + assertEquals(3, comicList.size()); + for (Iterator iterator = comicList.iterator(); iterator.hasNext(); ) { + ComicEntity next = iterator.next(); + comicImpl.delete(next); + } + comicList = comicImpl.findAll(); + assertNotNull(comicList); + assertEquals(0, comicList.size()); + } +} diff --git a/java/src/test/java/com/ibtp/kontor/comics/dal/IssueImplTest.java b/java/src/test/java/com/ibtp/kontor/comics/dal/IssueImplTest.java new file mode 100644 index 0000000..5797305 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/comics/dal/IssueImplTest.java @@ -0,0 +1,64 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.IssueEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class IssueImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testIssueAddAndDelete() { + String issueNumber = "42"; + IssueImpl issueImpl = new IssueImpl(); + IssueEntity issue = new IssueEntity(); + issue.setNumber(issueNumber); + issueImpl.store(issue); + Collection issueList = issueImpl.findByNumber(issueNumber); + assertNotNull(issueList); + assertEquals(1, issueList.size()); + issueImpl.delete(issue); + issueList = issueImpl.findByNumber(issueNumber); + assertNotNull(issueList); + assertEquals(0, issueList.size()); + } + + @Test + public void testIssueFindAll() { + IssueImpl issueImpl = new IssueImpl(); + IssueEntity issue1 = new IssueEntity(); + issue1.setNumber("issue1"); + IssueEntity issue2 = new IssueEntity(); + issue1.setNumber("issue2"); + IssueEntity issue3 = new IssueEntity(); + issue1.setNumber("issue3"); + issueImpl.store(issue1); + issueImpl.store(issue2); + issueImpl.store(issue3); + Collection issueList = issueImpl.findAll(); + assertNotNull(issueList); + assertEquals(3, issueList.size()); + for (IssueEntity issueEntity : issueList) { + issueImpl.delete(issueEntity); + } + issueList = issueImpl.findAll(); + assertNotNull(issueList); + assertEquals(0, issueList.size()); + } +} diff --git a/java/src/test/java/com/ibtp/kontor/comics/dal/PublisherImplTest.java b/java/src/test/java/com/ibtp/kontor/comics/dal/PublisherImplTest.java new file mode 100644 index 0000000..b795e96 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/comics/dal/PublisherImplTest.java @@ -0,0 +1,56 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.PublisherEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 20.01.2015. + */ +public class PublisherImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @AfterEach + public void cleanUp() { + PublisherImpl publisherImpl = new PublisherImpl(); + Collection publisherList = publisherImpl.findAll(); + for (PublisherEntity publisherEntity : publisherList) { + publisherImpl.delete(publisherEntity); + } + } + + @Test + public void testPublisherAddAndDelete() { + String publisherName = "testPublisherAddAndDelete"; + PublisherImpl publisherImpl = new PublisherImpl(); + PublisherEntity publisher = publisherImpl.addPublisher(publisherName); + Collection publisherList = publisherImpl.findByName(publisherName); + assertEquals(1, publisherList.size()); + publisherImpl.delete(publisher); + publisherList = publisherImpl.findByName(publisherName); + assertEquals(0, publisherList.size()); + } + + @Test + public void testPublisherFindAll() { + PublisherImpl publisherImpl = new PublisherImpl(); + publisherImpl.addPublisher("testDeletePublisher1"); + publisherImpl.addPublisher("testDeletePublisher2"); + publisherImpl.addPublisher("testDeletePublisher3"); + Collection publisherList = publisherImpl.findAll(); + assertEquals(3, publisherList.size()); + } +} diff --git a/java/src/test/java/com/ibtp/kontor/comics/dal/StoryArcImplTest.java b/java/src/test/java/com/ibtp/kontor/comics/dal/StoryArcImplTest.java new file mode 100644 index 0000000..2768e32 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/comics/dal/StoryArcImplTest.java @@ -0,0 +1,63 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.StoryArcEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Collection; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class StoryArcImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + StoryArcImpl storyArcImpl = new StoryArcImpl(); + Collection storyArcEntityCollection = storyArcImpl.findAll(); + for (StoryArcEntity storyArcEntity : storyArcEntityCollection) { + storyArcImpl.delete(storyArcEntity); + } + } + + @Test + public void testStoryArcAddAndDelete() { + String storyArcTtitle = "testStoryArcAddAndDelete"; + StoryArcImpl storyArcImpl = new StoryArcImpl(); + StoryArcEntity storyArc = new StoryArcEntity(); + storyArc.setTitle(storyArcTtitle); + storyArcImpl.store(storyArc); + Collection storyArcEntityCollection = storyArcImpl.findByTitle(storyArcTtitle); + assertNotNull(storyArcEntityCollection); + assertEquals(1, storyArcEntityCollection.size()); + storyArcImpl.delete(storyArc); + storyArcEntityCollection = storyArcImpl.findByTitle(storyArcTtitle); + assertNotNull(storyArcEntityCollection); + assertEquals(0, storyArcEntityCollection.size()); + } + + @Test + public void testStoryArcFindAll() { + StoryArcImpl storyArcImpl = new StoryArcImpl(); + StoryArcEntity storyArc; + storyArc = new StoryArcEntity(); + storyArc.setTitle("testStoryArcFindAll1"); + storyArcImpl.store(storyArc); + storyArc = new StoryArcEntity(); + storyArc.setTitle("testStoryArcFindAll2"); + storyArcImpl.store(storyArc); + storyArc = new StoryArcEntity(); + storyArc.setTitle("testStoryArcFindAll3"); + storyArcImpl.store(storyArc); + Collection storyArcEntityCollection = storyArcImpl.findAll(); + assertNotNull(storyArcEntityCollection); + assertEquals(3, storyArcEntityCollection.size()); + } +} diff --git a/java/src/test/java/com/ibtp/kontor/comics/dal/VolumeImplTest.java b/java/src/test/java/com/ibtp/kontor/comics/dal/VolumeImplTest.java new file mode 100644 index 0000000..fd34aa4 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/comics/dal/VolumeImplTest.java @@ -0,0 +1,72 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.VolumeEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class VolumeImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @AfterEach + public void cleanUp() { + VolumeImpl volumeImpl = new VolumeImpl(); + Collection volumeList = volumeImpl.findAll(); + for (VolumeEntity volumeEntity : volumeList) { + volumeImpl.delete(volumeEntity); + } + } + + @Test + public void testVolumeAddAndDelete() { + String volumeTitle = "testVolumeAddAndDelete"; + VolumeImpl volumeImpl = new VolumeImpl(); + VolumeEntity volume = new VolumeEntity(); + volume.setTitle(volumeTitle); + VolumeEntity volumeEntity = volumeImpl.store(volume); + assertNotNull(volumeEntity); + assertEquals(volumeTitle, volumeEntity.getTitle()); + Collection volumeList = volumeImpl.findByTitle(volumeTitle); + assertNotNull(volumeList); + assertEquals(1, volumeList.size()); + VolumeEntity result = (VolumeEntity)volumeList.toArray()[0]; + assertEquals(volume, result); + volumeImpl.delete(result); + volumeList = volumeImpl.findByTitle(volumeTitle); + assertEquals(0, volumeList.size()); + } + + @Test + public void testVolumeFindAll() { + VolumeImpl volumeImpl = new VolumeImpl(); + VolumeEntity volume; + volume = new VolumeEntity(); + volume.setTitle("testVolumeFindAll1"); + volumeImpl.store(volume); + volume = new VolumeEntity(); + volume.setTitle("testVolumeFindAll2"); + volumeImpl.store(volume); + volume = new VolumeEntity(); + volume.setTitle("testVolumeFindAll3"); + volumeImpl.store(volume); + Collection volumeList = volumeImpl.findAll(); + assertNotNull(volumeList); + assertEquals(3, volumeList.size()); + } +} diff --git a/java/src/test/java/com/ibtp/kontor/dal/DataAccessLayerTest.java b/java/src/test/java/com/ibtp/kontor/dal/DataAccessLayerTest.java new file mode 100644 index 0000000..a9737ac --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/dal/DataAccessLayerTest.java @@ -0,0 +1,63 @@ +package com.ibtp.kontor.dal; + +import static org.junit.jupiter.api.Assertions.fail; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +/** + * Created by TPEETZ on 10.02.2015. + */ +public class DataAccessLayerTest { + + public void findTests(String packageName, String entityName) { + String testClassName = packageName + entityName + "ImplTest"; + Class testClass; + try { + testClass = Class.forName(testClassName); + Method addAndDelete = testClass.getMethod("test" + entityName + "AddAndDelete"); + Method findAll = testClass.getMethod("test" + entityName + "FindAll"); + } catch (ClassNotFoundException e) { + fail("Class " + testClassName + " missing"); + } catch (NoSuchMethodException e) { + fail("Test method for class " + testClassName + " missing"); + } + } + + @Test + public void testFindComicTests() { + /* + * Find all Tests + */ + String[] testClasses = new String[]{"Artist", "Comic", "Issue", "Publisher", "StoryArc", "Volume"}; + for (int i = 0; i < testClasses.length; i++) { + String testEntity = testClasses[i]; + findTests("com.ibtp.kontor.comics.dal.", testEntity); + } + } + + @Test + public void testFindLibraryTests() { + /* + * Find all Tests + */ + String[] testClasses = new String[]{"Article", "Author", "Book", "File", "Title"}; + for (int i = 0; i < testClasses.length; i++) { + String testEntity = testClasses[i]; + findTests("com.ibtp.kontor.library.dal.", testEntity); + } + } + + @Test + public void testFindTradingCardsTests() { + /* + * Find all Tests + */ + String[] testClasses = new String[]{"BaseSet", "Insert", "Manufacturer", "ParallelSet", "Player", "Position", "SportCard", "Sport", "Team"}; + for (int i = 0; i < testClasses.length; i++) { + String testEntity = testClasses[i]; + findTests("com.ibtp.kontor.tradingcards.dal." , testEntity); + } + } +} diff --git a/java/src/test/java/com/ibtp/kontor/library/BookshelfTest.java b/java/src/test/java/com/ibtp/kontor/library/BookshelfTest.java new file mode 100644 index 0000000..f6609fb --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/library/BookshelfTest.java @@ -0,0 +1,23 @@ +package com.ibtp.kontor.library; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class BookshelfTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testAddAuthors() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/library/dal/ArticleImplTest.java b/java/src/test/java/com/ibtp/kontor/library/dal/ArticleImplTest.java new file mode 100644 index 0000000..3fc8772 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/library/dal/ArticleImplTest.java @@ -0,0 +1,57 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.ibtp.kontor.library.entity.ArticleEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +public class ArticleImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + cleanUp(); + } + + @AfterAll + public static void cleanUp() { + ArticleImpl articleImpl = new ArticleImpl(); + Collection articleList = articleImpl.findAll(); + for (ArticleEntity articleEntity : articleList) { + articleImpl.delete(articleEntity); + } + } + + @Test + public void testAddArticle() { + String articleTitle = "testAddArticle"; + ArticleImpl articleImpl = new ArticleImpl(); + ArticleEntity article = articleImpl.addArticle(articleTitle); + assertNotNull(article); + List articleList = articleImpl.findByTitle(articleTitle); + assertEquals(1, articleList.size()); + } + + @Test + public void testArticleAddAndDelete() { + + } + + @Test + public void testArticleFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/library/dal/AuthorImplTest.java b/java/src/test/java/com/ibtp/kontor/library/dal/AuthorImplTest.java new file mode 100644 index 0000000..cec2048 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/library/dal/AuthorImplTest.java @@ -0,0 +1,56 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.ibtp.kontor.library.entity.AuthorEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +/** + * Created by thomas on 23.01.15. + */ +public class AuthorImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testAddAuthor() { + String authorName = "testAddAuthor"; + AuthorImpl authorImpl = new AuthorImpl(); + AuthorEntity author = authorImpl.addAuthor(authorName); + assertNotNull(author); + } + + @Test + public void testDeleteAuthor() { + String authorName = "testDeleteAuthor"; + AuthorImpl authorImpl = new AuthorImpl(); + AuthorEntity author = authorImpl.addAuthor(authorName); + assertNotNull(author); + List authorList = authorImpl.findByName(authorName); + assertEquals(1, authorList.size()); + authorImpl.delete(author); + authorList = authorImpl.findByName(authorName); + assertEquals(0, authorList.size()); + } + + @Test + public void testAuthorAddAndDelete() { + + } + + @Test + public void testAuthorFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/library/dal/BookImplTest.java b/java/src/test/java/com/ibtp/kontor/library/dal/BookImplTest.java new file mode 100644 index 0000000..95080e8 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/library/dal/BookImplTest.java @@ -0,0 +1,28 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class BookImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testBookAddAndDelete() { + + } + + @Test + public void testBookFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/library/dal/FileImplTest.java b/java/src/test/java/com/ibtp/kontor/library/dal/FileImplTest.java new file mode 100644 index 0000000..dd0c690 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/library/dal/FileImplTest.java @@ -0,0 +1,28 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class FileImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testFileAddAndDelete() { + + } + + @Test + public void testFileFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/library/dal/TitleImplTest.java b/java/src/test/java/com/ibtp/kontor/library/dal/TitleImplTest.java new file mode 100644 index 0000000..92acd55 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/library/dal/TitleImplTest.java @@ -0,0 +1,28 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class TitleImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testTitleAddAndDelete() { + + } + + @Test + public void testTitleFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/tradingcards/CollectionTest.java b/java/src/test/java/com/ibtp/kontor/tradingcards/CollectionTest.java new file mode 100644 index 0000000..69e28ad --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/tradingcards/CollectionTest.java @@ -0,0 +1,168 @@ +package com.ibtp.kontor.tradingcards; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.tradingcards.dal.TeamImpl; +import com.ibtp.kontor.tradingcards.entity.TeamEntity; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.ibtp.kontor.tradingcards.dal.SportImpl; +import com.ibtp.kontor.tradingcards.entity.SportEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class CollectionTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + setupSports(); + } + + public static void setupSports() { + SportImpl sportImpl = new SportImpl(); + SportEntity football = sportImpl.addSport("Football"); + setupFootballTeams(football); + SportEntity baseball = sportImpl.addSport("Baseball"); + setupFootballTeams(baseball); + SportEntity basketball = sportImpl.addSport("Basketball"); + setupBasketballTeams(basketball); + SportEntity hockey = sportImpl.addSport("Hockey"); + setupHockeyTeams(hockey); + } + + public static void setupFootballTeams(SportEntity football) { + TeamImpl teamImpl = new TeamImpl(); + teamImpl.addTeam("Dallas Cowboys", football); + teamImpl.addTeam("New York Giants", football); + teamImpl.addTeam("Philadelphia Eagles", football); + teamImpl.addTeam("Arizona Cardinals", football); + teamImpl.addTeam("Washington Redskins", football); + teamImpl.addTeam("Detroit Lions", football); + teamImpl.addTeam("Minnesota Vikings", football); + teamImpl.addTeam("Green Bay Packers", football); + teamImpl.addTeam("Chicago Bears", football); + teamImpl.addTeam("Tampa Bay Buccaneers", football); + teamImpl.addTeam("San Francisco 49ers", football); + teamImpl.addTeam("New Orleans Saints", football); + teamImpl.addTeam("Atlanta Falcons", football); + teamImpl.addTeam("Los Angeles Rams", football); + teamImpl.addTeam("Buffalo Bills", football); + teamImpl.addTeam("Miami Dolphins", football); + teamImpl.addTeam("New York Jets", football); + teamImpl.addTeam("New England Patriots", football); + teamImpl.addTeam("Indianapolis Colts", football); + teamImpl.addTeam("Houston Oilers", football); + teamImpl.addTeam("Pittsburgh Steelers", football); + teamImpl.addTeam("Cleveland Browns", football); + teamImpl.addTeam("Kansas City Chiefs", football); + teamImpl.addTeam("Los Angeles Raiders", football); + teamImpl.addTeam("Denver Broncos", football); + teamImpl.addTeam("San Diego Chargers", football); + teamImpl.addTeam("Seattle Seahawks", football); + teamImpl.addTeam("Jacksonville Jaguars", football); + teamImpl.addTeam("Houston Texans", football); + } + + public static void setupBaseballTeams(SportEntity baseball) { + TeamImpl teamImpl = new TeamImpl(); + } + + public static void setupBasketballTeams(SportEntity basketball) { + TeamImpl teamImpl = new TeamImpl(); + teamImpl.addTeam("Houston Rockets", basketball); + teamImpl.addTeam("San Antonio Spurs", basketball); + teamImpl.addTeam("Utah Jazz", basketball); + teamImpl.addTeam("Denver Nuggets", basketball); + teamImpl.addTeam("Minnesota Timberwolves", basketball); + teamImpl.addTeam("Dallas Mavericks", basketball); + teamImpl.addTeam("Seattle SuperSonics", basketball); + teamImpl.addTeam("Phoenix Suns", basketball); + teamImpl.addTeam("Golden State Warriors", basketball); + teamImpl.addTeam("Portland Trail Blazers", basketball); + teamImpl.addTeam("Los Angeles Lakers", basketball); + teamImpl.addTeam("Sacramento Kings", basketball); + teamImpl.addTeam("Los Angeles Clippers", basketball); + teamImpl.addTeam("New York Knicks", basketball); + teamImpl.addTeam("Orlando Magic", basketball); + teamImpl.addTeam("New Jersey Nets", basketball); + teamImpl.addTeam("Miami Heat", basketball); + teamImpl.addTeam("Boston Celtics", basketball); + teamImpl.addTeam("Philadelphia 76ers", basketball); + teamImpl.addTeam("Washington Bullets", basketball); + teamImpl.addTeam("Atlanta Hawks", basketball); + teamImpl.addTeam("Chicago Bulls", basketball); + teamImpl.addTeam("Indiana Pacers", basketball); + teamImpl.addTeam("Cleveland Cavaliers", basketball); + teamImpl.addTeam("Charlotte Hornets", basketball); + teamImpl.addTeam("Detroit Pistons", basketball); + teamImpl.addTeam("Milwaukee Bucks", basketball); + } + + public static void setupHockeyTeams(SportEntity hockey) { + TeamImpl teamImpl = new TeamImpl(); + teamImpl.addTeam("New York Rangers", hockey); + teamImpl.addTeam("Buffalo Sabers", hockey); + teamImpl.addTeam("Detroit Red Wings", hockey); + teamImpl.addTeam("Vancouver Canucks", hockey); + teamImpl.addTeam("Mighty Ducks of Anaheim", hockey); + teamImpl.addTeam("Calgary Flames", hockey); + teamImpl.addTeam("Edmonton Oilers", hockey); + teamImpl.addTeam("Los Angeles Kings", hockey); + teamImpl.addTeam("San Jose Sharks", hockey); + teamImpl.addTeam("Chicago Blackhawks", hockey); + teamImpl.addTeam("Dallas Stars", hockey); + teamImpl.addTeam("St. Louis Blues", hockey); + teamImpl.addTeam("Toronto Maple Leafs", hockey); + teamImpl.addTeam("Winnipeg Jets", hockey); + teamImpl.addTeam("Boston Bruins", hockey); + teamImpl.addTeam("Hartford Whalers", hockey); + teamImpl.addTeam("Montreal Canadiers", hockey); + teamImpl.addTeam("Ottawa Senators", hockey); + teamImpl.addTeam("Pittsburgh Penguins", hockey); + teamImpl.addTeam("Quebec Nordiques", hockey); + teamImpl.addTeam("Florida Panthers", hockey); + teamImpl.addTeam("New Jersey Devils", hockey); + teamImpl.addTeam("New York Islanders", hockey); + teamImpl.addTeam("Philadelphia Flyers", hockey); + teamImpl.addTeam("Tamba Bay Lightning", hockey); + teamImpl.addTeam("Washington Capitals", hockey); + } + + @AfterAll + public static void tearDown() { + TeamImpl teamImpl = new TeamImpl(); + Collection teamEntities = teamImpl.findAll(); + for (TeamEntity teamEntity : teamEntities) { + teamImpl.delete(teamEntity); + } + SportImpl sportImpl = new SportImpl(); + Collection sportEntities = sportImpl.findAll(); + for (SportEntity sportEntity : sportEntities) { + sportImpl.delete(sportEntity); + } + } + + @Test + public void gettAllSports() { + SportImpl sportImpl = new SportImpl(); + Collection resultList = sportImpl.findAll(); + assertEquals(4, resultList.size()); + } + + @Test + public void getAllTeams() { + TeamImpl teamImpl = new TeamImpl(); + Collection resultList = teamImpl.findAll(); + assertEquals(111, resultList.size()); + } +} diff --git a/java/src/test/java/com/ibtp/kontor/tradingcards/dal/BaseSetImplTest.java b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/BaseSetImplTest.java new file mode 100644 index 0000000..9f3bb4c --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/BaseSetImplTest.java @@ -0,0 +1,28 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class BaseSetImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testBaseSetAddAndDelete() { + + } + + @Test + public void testBaseSetFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/tradingcards/dal/InsertImplTest.java b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/InsertImplTest.java new file mode 100644 index 0000000..c246743 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/InsertImplTest.java @@ -0,0 +1,29 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class InsertImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + + @Test + public void testInsertAddAndDelete() { + + } + + @Test + public void testInsertFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImplTest.java b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImplTest.java new file mode 100644 index 0000000..3681a3f --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImplTest.java @@ -0,0 +1,57 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.ibtp.kontor.tradingcards.entity.ManufacturerEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +/** + * Created by tpeetz on 20.01.2015. + */ +public class ManufacturerImplTest { + + @BeforeAll + public static void setup() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void addManufacturer() { + String manufacturerName = "Manufacturer1"; + ManufacturerImpl manufacturerImpl = new ManufacturerImpl(); + ManufacturerEntity manufacturer = manufacturerImpl.addManufacturer(manufacturerName); + assertNotNull(manufacturer); + List manufacturerList = manufacturerImpl.findByName(manufacturerName); + assertTrue(manufacturerList.size() > 0); + } + + @Test + public void deleteManufacturer() { + String manufacturerName = "Manufacturer1"; + ManufacturerImpl manufacturerImpl = new ManufacturerImpl(); + List manufacturerList = manufacturerImpl.findByName(manufacturerName); + assertTrue(manufacturerList.size() > 0); + manufacturerImpl.delete(manufacturerList.get(0)); + manufacturerList = manufacturerImpl.findByName(manufacturerName); + assertEquals(0, manufacturerList.size()); + } + + @Test + public void testManufacturerAddAndDelete() { + + } + + @Test + public void testManufacturerFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImplTest.java b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImplTest.java new file mode 100644 index 0000000..b674dd1 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImplTest.java @@ -0,0 +1,28 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class ParallelSetImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testParallelSetAddAndDelete() { + + } + + @Test + public void testParallelSetFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/tradingcards/dal/PlayerImplTest.java b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/PlayerImplTest.java new file mode 100644 index 0000000..bee6239 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/PlayerImplTest.java @@ -0,0 +1,28 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class PlayerImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testPlayerAddAndDelete() { + + } + + @Test + public void testPlayerFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/tradingcards/dal/PositionImplTest.java b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/PositionImplTest.java new file mode 100644 index 0000000..9271707 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/PositionImplTest.java @@ -0,0 +1,44 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.ibtp.kontor.tradingcards.entity.PositionEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collection; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class PositionImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testPositionAddAndDelete() { + String positionName = "testPositionAddAndDelete"; + PositionImpl positionImpl = new PositionImpl(); + PositionEntity position = positionImpl.addPosition(positionName); + Collection resultList = positionImpl.findByName(positionName); + assertEquals(1, resultList.size()); + positionImpl.delete(position); + resultList = positionImpl.findByName(positionName); + assertEquals(0, resultList.size()); + } + + @Test + public void testPositionFindAll() { + PositionImpl positionImpl = new PositionImpl(); + Collection resultList = positionImpl.findAll(); + assertEquals(0, resultList.size()); + } +} + diff --git a/java/src/test/java/com/ibtp/kontor/tradingcards/dal/SportCardImplTest.java b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/SportCardImplTest.java new file mode 100644 index 0000000..6cc8b5c --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/SportCardImplTest.java @@ -0,0 +1,28 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class SportCardImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testSportCardAddAndDelete() { + + } + + @Test + public void testSportCardFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/tradingcards/dal/SportImplTest.java b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/SportImplTest.java new file mode 100644 index 0000000..6a4d580 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/SportImplTest.java @@ -0,0 +1,44 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.ibtp.kontor.tradingcards.entity.SportEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +public class SportImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testSportAddAndDelete() { + String sportName = "testSportAddAndDelete"; + SportImpl sportImpl = new SportImpl(); + SportEntity sport = sportImpl.addSport(sportName); + List sportList = sportImpl.findByName(sportName); + assertEquals(1, sportList.size()); + sportImpl.delete(sport); + List result = sportImpl.findByName(sportName); + assertEquals(0, result.size()); + } + + @Test + public void testSportFindAll() { + SportImpl sportImpl = new SportImpl(); + Collection resultList = sportImpl.findAll(); + assertEquals(0, resultList.size()); + } +} diff --git a/java/src/test/java/com/ibtp/kontor/tradingcards/dal/TeamImplTest.java b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/TeamImplTest.java new file mode 100644 index 0000000..7e94a1b --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/TeamImplTest.java @@ -0,0 +1,44 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.ibtp.kontor.tradingcards.entity.TeamEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collection; + +/** + * Created by tpeetz on 20.01.2015. + */ +public class TeamImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testTeamAddAndDelete() { + String teamName = "testTeamAddAndDelete"; + TeamEntity team = new TeamEntity(teamName); + TeamImpl teamImpl = new TeamImpl(); + teamImpl.store(team); + Collection resultList = teamImpl.findByName(teamName); + assertEquals(1, resultList.size()); + teamImpl.delete(team); + resultList = teamImpl.findByName(teamName); + assertEquals(0, resultList.size()); + } + + @Test + public void testTeamFindAll() { + TeamImpl teamImpl = new TeamImpl(); + Collection resultList = teamImpl.findAll(); + assertEquals(0, resultList.size()); + } +} diff --git a/java/src/test/java/com/ibtp/kontor/util/LocalTestDatabase.java b/java/src/test/java/com/ibtp/kontor/util/LocalTestDatabase.java new file mode 100644 index 0000000..503104a --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/util/LocalTestDatabase.java @@ -0,0 +1,110 @@ +package com.ibtp.kontor.util; + +import com.ibtp.kontor.dal.Database; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hsqldb.Server; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import javax.persistence.spi.PersistenceProvider; +import javax.persistence.spi.PersistenceProviderResolver; +import javax.persistence.spi.PersistenceProviderResolverHolder; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Created by TPEETZ on 21.01.2015. + */ +public class LocalTestDatabase implements Database { + + private static Server server; + private static EntityManagerFactory factory; + private static EntityManager em; + private static Logger logger = LoggerFactory.getLogger(LocalTestDatabase.class.getName()); + + static { + logger.info("initialization and starting database"); + LocalTestDatabase.assureDatabaseRunning(); + } + + public LocalTestDatabase() { + logger.info("LocalDatabaseTest started"); + } + + private static void assureDatabaseRunning() { + if (LocalTestDatabase.server == null) { + LocalTestDatabase.startDatabase(); + } + } + + private static void startDatabase() { + logger.info("startDatabase as kontor in hsqldb_databases/test"); + LocalTestDatabase.server = new Server(); + LocalTestDatabase.server.setAddress("localhost"); + LocalTestDatabase.server.setDatabaseName(0, "kontor"); + LocalTestDatabase.server.setDatabasePath(0, "file:build/hsqldb_databases/test"); + LocalTestDatabase.server.setPort(2345); + LocalTestDatabase.server.setTrace(true); + LocalTestDatabase.server.setLogWriter(new PrintWriter(System.out)); + LocalTestDatabase.server.start(); + } + + private static void stopDatabase() { + server.shutdown(); + } + + private static EntityManagerFactory getFactory() { + if (LocalTestDatabase.factory == null) { + LocalTestDatabase.assureDatabaseRunning(); + PersistenceProviderResolverHolder.setPersistenceProviderResolver(new PersistenceProviderResolver() { + private final List providers_ = Arrays.asList((PersistenceProvider) new HibernatePersistenceProvider()); + + @Override + public void clearCachedProviders() { + // Auto-generated method stub + } + + @Override + public List getPersistenceProviders() { + return providers_; + } + }); + LocalTestDatabase.factory = Persistence.createEntityManagerFactory("com.ibtp.kontor"); + logger.info("EntityManagerFactory(com.ibtp.kontor) created"); + } + return factory; + } + + private static EntityManager getSingleEntityManager() { + return LocalTestDatabase.em; + } + + private static void setSingleEntityManager(EntityManager manager) { + LocalTestDatabase.em = manager; + } + + @Override + public EntityManager getEntityManager() { + if (getSingleEntityManager() == null) { + setSingleEntityManager(getFactory().createEntityManager()); + logger.info("EntityManager created"); + } + return getSingleEntityManager(); + } + + @Override + public String toString() { + String serverMessage; + if (LocalTestDatabase.server == null) { + serverMessage = "server:null"; + } else { + serverMessage = LocalTestDatabase.server.toString(); + } + return LocalTestDatabase.class.getName() + " " + serverMessage; + } +} diff --git a/java/src/test/resources/META-INF/persistence.xml b/java/src/test/resources/META-INF/persistence.xml new file mode 100644 index 0000000..35d671b --- /dev/null +++ b/java/src/test/resources/META-INF/persistence.xml @@ -0,0 +1,39 @@ + + + org.hibernate.jpa.HibernatePersistenceProvider + com.ibtp.kontor.comics.entity.ArtistEntity + com.ibtp.kontor.comics.entity.ComicEntity + com.ibtp.kontor.comics.entity.IssueEntity + com.ibtp.kontor.comics.entity.StoryArcEntity + com.ibtp.kontor.comics.entity.VolumeEntity + com.ibtp.kontor.comics.entity.PublisherEntity + com.ibtp.kontor.library.entity.AuthorEntity + com.ibtp.kontor.library.entity.ArticleEntity + com.ibtp.kontor.library.entity.BookEntity + com.ibtp.kontor.library.entity.FileEntity + com.ibtp.kontor.library.entity.TitleEntity + com.ibtp.kontor.tradingcards.entity.SportEntity + com.ibtp.kontor.tradingcards.entity.TeamEntity + com.ibtp.kontor.tradingcards.entity.PositionEntity + com.ibtp.kontor.tradingcards.entity.PlayerEntity + com.ibtp.kontor.tradingcards.entity.ManufacturerEntity + com.ibtp.kontor.tradingcards.entity.BaseSetEntity + com.ibtp.kontor.tradingcards.entity.InsertEntity + com.ibtp.kontor.tradingcards.entity.ParallelSetEntity + com.ibtp.kontor.tradingcards.entity.SportCardEntity + + + + + + + + + + + + + diff --git a/java/src/test/resources/logback.xml b/java/src/test/resources/logback.xml new file mode 100644 index 0000000..5254e50 --- /dev/null +++ b/java/src/test/resources/logback.xml @@ -0,0 +1,41 @@ + + + + + + %d{yyyy-MM-dd_HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + + + + + build/kontortest.log + + %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + build/kontortest.%i.log.zip + 1 + 10 + + + + 2MB + + + + + + + + + + + + + + diff --git a/java/tysc-20041010-1819.sql b/java/tysc-20041010-1819.sql new file mode 100644 index 0000000..167c41d --- /dev/null +++ b/java/tysc-20041010-1819.sql @@ -0,0 +1,168 @@ +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE=NO_AUTO_VALUE_ON_ZERO */; +CREATE DATABASE /*!32312 IF NOT EXISTS*/ `tysc`; +USE `tysc`; + +DROP TABLE IF EXISTS `angebote`; +CREATE TABLE `angebote` ( + `user_id` int(11) NOT NULL default '0', + `karte_id` int(11) NOT NULL default '0' +) TYPE=MyISAM; +INSERT INTO `angebote` (`user_id`,`karte_id`) VALUES (3,28),(3,30); + +DROP TABLE IF EXISTS `benutzer`; +CREATE TABLE `benutzer` ( + `ID` int(11) NOT NULL auto_increment, + `forename` varchar(40) default NULL, + `surname` varchar(40) default NULL, + `strasse` varchar(60) default NULL, + `plz` int(6) default NULL, + `ort` varchar(20) default NULL, + `username` varchar(20) default NULL, + `email` varchar(60) default NULL, + `password` varchar(32) default NULL, + `java` char(1) default NULL, + `language` int(11) NOT NULL default '0', + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `benutzer` (`ID`,`forename`,`surname`,`strasse`,`plz`,`ort`,`username`,`email`,`password`,`java`,`language`) VALUES (1,'Thomas','Peetz','Reichweindamm 24',13627,'Berlin','gophard','thomas.peetz@snafu.de','t.log1n','N',1),(2,'Heiko','John','Johannastr.49',13581,'Berlin','John','healjo@hotmail.com','redskins','N',1),(3,'Thomas','Peetz','Reichweindamm 24',13627,'Berlin','peetz','gophard@snafu.de','peetz','N',1); + +DROP TABLE IF EXISTS `changelog`; +CREATE TABLE `changelog` ( + `datum` date default NULL, + `tablename` varchar(20) default NULL, + `id` int(11) NOT NULL default '0' +) TYPE=MyISAM; +INSERT INTO `changelog` (`datum`,`tablename`,`id`) VALUES ('2002-02-27','benutzer',1),('2002-02-28','benutzer',2),('2002-03-05','benutzer',3),('2002-03-05','angebote',0),('2002-03-05','angebote',0); + +DROP TABLE IF EXISTS `hersteller`; +CREATE TABLE `hersteller` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(30) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `hersteller` (`ID`,`name`) VALUES (1,'Pacific'),(2,'Fleer'),(3,'Bowman'),(6,'Topps'),(7,'Donruss'),(8,'Score'),(9,'Flair'); + +DROP TABLE IF EXISTS `inserts`; +CREATE TABLE `inserts` ( + `ID` int(11) NOT NULL auto_increment, + `hersteller_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`,`hersteller_id`) +) TYPE=MyISAM; +INSERT INTO `inserts` (`ID`,`hersteller_id`,`name`) VALUES (1,2,'Mystique Big Buzz'); + +DROP TABLE IF EXISTS `karte`; +CREATE TABLE `karte` ( + `ID` int(11) NOT NULL auto_increment, + `spieler_id` int(11) NOT NULL default '0', + `team_id` int(11) NOT NULL default '0', + `hersteller_id` int(11) NOT NULL default '0', + `serie_id` int(11) default NULL, + `parallel_id` int(11) default NULL, + `inserts_id` int(11) default NULL, + `rookie` char(1) default NULL, + `jahr` int(4) default NULL, + `nummer` int(11) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `karte` (`ID`,`spieler_id`,`team_id`,`hersteller_id`,`serie_id`,`parallel_id`,`inserts_id`,`rookie`,`jahr`,`nummer`) VALUES (12,12,13,1,1,0,0,'N',2001,212),(1,1,2,1,1,0,0,'N',2001,185),(2,2,2,1,1,0,0,'N',2001,250),(3,3,8,1,1,0,0,'N',2001,103),(4,4,8,1,1,0,0,'N',2001,112),(5,5,6,1,1,0,0,'N',2001,37),(6,6,6,1,1,0,0,'N',2001,38),(7,7,6,1,1,0,0,'N',2001,31),(8,8,10,1,1,0,0,'N',2001,338),(9,9,10,1,1,0,0,'N',2001,335),(10,10,10,1,1,0,0,'N',2001,345),(11,11,13,1,1,0,0,'N',2001,213),(13,13,14,1,1,0,0,'N',2001,311),(14,14,14,1,1,0,0,'N',2001,312),(15,15,16,1,1,0,0,'N',2001,403),(16,16,16,1,1,0,0,'N',2001,397),(17,17,16,1,1,0,0,'N',2001,404),(18,18,18,1,1,0,0,'N',2001,116),(19,19,18,1,1,0,0,'N',2001,122),(20,20,18,1,1,0,0,'N',2001,117),(21,21,19,1,1,0,0,'N',2001,281),(22,22,19,1,1,0,0,'N',2001,321),(23,23,20,1,1,0,0,'N',2001,331),(24,24,20,1,1,0,0,'N',2001,324),(25,25,21,1,1,0,0,'N',2001,445),(26,26,27,1,1,0,0,'N',2001,28),(27,27,27,1,1,0,0,'N',2001,17),(28,28,27,1,1,0,0,'N',2001,23),(29,29,29,1,1,0,0,'N',2001,273); +INSERT INTO `karte` (`ID`,`spieler_id`,`team_id`,`hersteller_id`,`serie_id`,`parallel_id`,`inserts_id`,`rookie`,`jahr`,`nummer`) VALUES (30,30,31,1,1,0,0,'N',2001,380),(31,31,31,1,1,0,0,'N',2001,390),(32,32,31,1,1,0,0,'N',2001,381),(33,33,31,1,1,0,0,'N',2001,387),(34,34,31,1,1,0,0,'N',2001,386),(35,35,30,1,1,0,0,'N',2001,349),(36,36,30,1,1,0,0,'N',2001,350),(37,37,44,5,8,0,0,'N',1994,106); + +DROP TABLE IF EXISTS `language`; +CREATE TABLE `language` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(15) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `mannschaft`; +CREATE TABLE `mannschaft` ( + `ID` int(11) NOT NULL auto_increment, + `team_id` int(11) NOT NULL default '0', + `sportart_id` int(11) NOT NULL default '0', + PRIMARY KEY (`ID`) +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `parallelset`; +CREATE TABLE `parallelset` ( + `ID` int(11) NOT NULL auto_increment, + `hersteller_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`,`hersteller_id`) +) TYPE=MyISAM; +INSERT INTO `parallelset` (`ID`,`hersteller_id`,`name`) VALUES (1,2,'Mystique Gold'),(2,1,'Pacific Copper'),(3,1,'Pacific Gold'); + +DROP TABLE IF EXISTS `position`; +CREATE TABLE `position` ( + `ID` int(11) NOT NULL auto_increment, + `sportart_id` int(11) NOT NULL default '0', + `name` varchar(20) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `position` (`ID`,`sportart_id`,`name`) VALUES (1,1,'QB'),(2,1,'WR'),(3,1,'RB'),(4,1,'LB'),(5,1,'TE'),(6,1,'FB'),(7,1,'SS'),(8,1,'DE'),(9,1,'K'),(10,1,'P'),(11,1,'LG'),(12,1,'RG'),(13,1,'OF'),(14,1,'DB'),(15,1,'CB'),(16,2,'C'),(17,2,'1B'),(18,2,'2B'),(19,2,'3B'),(20,2,'SS'),(21,2,'LF'),(22,2,'CF'),(23,2,'RF'),(24,2,'DH'),(25,2,'P'); + +DROP TABLE IF EXISTS `serie`; +CREATE TABLE `serie` ( + `ID` int(11) NOT NULL auto_increment, + `hersteller_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`,`hersteller_id`) +) TYPE=MyISAM; +INSERT INTO `serie` (`ID`,`hersteller_id`,`name`) VALUES (1,1,'Pacific'),(2,2,'Fleer'),(3,3,'Bowman'),(4,4,'Leaf'),(5,2,'Ultra'),(6,2,'Mystique'),(7,1,'Finest Hour'),(8,5,'SP'),(9,5,'SPX'),(10,5,'SP Authentic'),(11,5,'Black Diamond'); + +DROP TABLE IF EXISTS `spiele`; +CREATE TABLE `spiele` ( + `datum` date default NULL, + `gast` int(11) NOT NULL default '0', + `gast_pkt` int(11) default NULL, + `heim` int(11) NOT NULL default '0', + `heim_pkt` int(11) default NULL +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `spieler`; +CREATE TABLE `spieler` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `spieler` (`ID`,`name`) VALUES (1,'Pathon, Jerome'),(2,'Bruschi, Tedy'),(3,'Couch, Tim'),(4,'Shea, Aaron'),(5,'Lewis, Jamal'),(6,'Lewis, Jermaine'),(7,'Banks, Tony'),(8,'Fuamatu-Ma\'Afala, Chris'),(9,'Bettis, Jerome'),(10,'Stewart, Kordell'),(11,'Moon, Warren'),(12,'Lockett, Kevin'),(13,'Gannon, Rich'),(14,'Jett, James'),(15,'Strong, Mack'),(16,'Huard, Brock'),(17,'Watters, Ricky'),(18,'Aikman, Troy'),(19,'LaFleur, David'),(20,'Brazzell, Chris'),(21,'Dayne, Ron'),(22,'Brown, Na'),(23,'Small, Torrance'),(24,'Lewis, Chad'),(25,'Murrell, Adrian'),(26,'Smith, Maurice'),(27,'Chandler, Chris'),(28,'Kanell, Danny'),(29,'Williams, Ricky'),(30,'Garcia, Jeff'),(31,'Streets, Tai'),(32,'Garner, Charlie'),(33,'Rice, Jerry'),(34,'Owens, Terrell'),(35,'Bruce, Isaac'),(36,'Canidate, Trung'); + +DROP TABLE IF EXISTS `spielerposition`; +CREATE TABLE `spielerposition` ( + `spieler_id` int(11) NOT NULL default '0', + `sportart_id` int(11) NOT NULL default '0', + `position_id` int(11) NOT NULL default '0' +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `sportart`; +CREATE TABLE `sportart` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(30) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `sportart` (`ID`,`name`) VALUES (1,'Football'),(2,'Baseball'),(3,'Basketball'),(4,'Hockey'); + +DROP TABLE IF EXISTS `suche`; +CREATE TABLE `suche` ( + `user_id` int(11) NOT NULL default '0', + `karte_id` int(11) NOT NULL default '0' +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `team`; +CREATE TABLE `team` ( + `ID` int(11) NOT NULL auto_increment, + `sportart_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + `short` varchar(15) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (1,1,'Buffalo Bills','Bills'),(2,1,'Indianapolis Colts','Colts'),(3,1,'Miami Dolphins','Dolphins'),(4,1,'New England Patriots','Patriots'),(5,1,'New York Jets','Jets'),(6,1,'Baltimore Ravens','Ravens'),(7,1,'Cincinnati Bengals','Bengals'),(8,1,'Cleveland Browns','Browns'),(9,1,'Jacksonville Jaguars','Jaguars'),(10,1,'Pittsburgh Steelers','Steelers'),(11,1,'Tennessee Titans','Titans'),(12,1,'Denver Broncos','Broncos'),(13,1,'Kansas City Chiefs','Chiefs'),(14,1,'Oakland Raiders','Raiders'),(15,1,'San Diego Chargers','Chargers'),(16,1,'Seattle Seahawks','Seahawks'),(17,1,'Arizona Cardinals','Cardinals'),(18,1,'Dallas Cowboys','Cowboys'),(19,1,'New York Giants','Giants'),(20,1,'Philadelphia Eagles','Eagles'),(21,1,'Washington Redskins','Redskins'),(22,1,'Chicago Bears','Bears'),(23,1,'Detroit Lions','Lions'),(24,1,'Green Bay Packers','Packers'),(25,1,'Minnesota Vikings','Vikings'),(26,1,'Tampa Bay Buccaneers','Buccaneers'),(27,1,'Atlanta Falcons','Falcons'),(28,1,'Carolina Panthers','Panthers'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (29,1,'New Orleans Saints','Saints'),(30,1,'St.Louis Rams','Rams'),(31,1,'San Francisco 49ers','49ers'),(32,2,'Baltimore Orioles','Orioles'),(33,2,'Boston Red Sox','Red Sox'),(34,2,'New York Yankees','Yankees'),(35,2,'Tampa Bay Devil Rays','Devil Rays'),(36,2,'Toronto Blue Jays','Blue Jays'),(37,2,'Chicago White Sox','White Sox'),(38,2,'Cleveland Indians','Indians'),(39,2,'Detroit Tigers','Tigers'),(40,2,'Kansas City Royals','Royals'),(41,2,'Minnesota Twins','Twins'),(42,2,'Anaheim Angels','Angels'),(43,2,'Oakland Athletics','Athletics'),(44,2,'Seattle Mariners','Mariners'),(45,2,'Texas Rangers','Rangers'),(46,2,'Atlanta Braves','Braves'),(47,2,'Florida Marlins','Marlins'),(48,2,'Montreal Expos','Expos'),(49,2,'New York Mets','Mets'),(50,2,'Philadelphia Phillies','Phillies'),(51,2,'Chicago Cubs','Cubs'),(52,2,'Cincinnati Reds','Reds'),(53,2,'Houston Astros','Astros'),(54,2,'Milwaukee Brewers','Brewers'),(55,2,'Pittsburgh Pirates','Pirates'),(56,2,'St.Louis Cardinals','Cardinals'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (57,2,'Arizona Diamondbacks','Diamondbacks'),(58,2,'Colorado Rockies','Rockies'),(59,2,'Los Angeles Dodgers','Dodgers'),(60,2,'San Diego Padres','Padres'),(61,2,'San Francisco Giants','Giants'),(62,3,'Boston Celtics','Celtics'),(63,3,'Miami Heat','Heat'),(64,3,'New Jersey Nets','Mets'),(65,3,'New York Knicks','Knicks'),(66,3,'Orlando Magic','Magic'),(67,3,'Philadelphia 76ers','76ers'),(68,3,'Washington Wizards','Wizards'),(69,3,'Atlanta Hawks','Hawks'),(70,3,'Charlotte Hornets','Hornets'),(71,3,'Chicago Bulls','Bulls'),(72,3,'Cleveland Cavaliers','Cavaliers'),(73,3,'Detroit Pistons','Pistons'),(74,3,'Indiana Pacers','Pacers'),(75,3,'Milwaukee Bucks','Bucks'),(76,3,'Toronto Raptors','Raptors'),(77,3,'Dallas Mavericks','Mavericks'),(78,3,'Denver Nuggets','Nuggets'),(79,3,'Houston Rockets','Rockets'),(80,3,'Minnesota Timberwolves','Timberwolves'),(81,3,'San Antonio Spurs','Spurs'),(82,3,'Utah Jazz','Jazz'),(83,3,'Vancouver Grizzlies','Grizzlies'),(84,3,'Golden State Warriors','Warriors'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (85,3,'Los Angeles Clippers','Clippers'),(86,3,'Los Angeles Lakers','Lakers'),(87,3,'Phoenix Suns','Suns'),(88,3,'Portland Trail Blazers','Blazers'),(89,3,'Sacramento Kings','Kings'),(90,3,'Seattle SuperSonics','SuperSonics'),(91,4,'Boston Bruins','Bruins'),(92,4,'Buffalo Sabres','Sabres'),(93,4,'Montreal Canadiens','Canadiens'),(94,4,'Ottawa Senators','Senators'),(95,4,'Toronto Maple Leafs','Maple Leafs'),(96,4,'New Jersey Devils','Devils'),(97,4,'New York Islander','Islander'),(98,4,'New York Rangers','Rangers'),(99,4,'Philadelphia Flyers','Flyers'),(100,4,'Pittsburgh Penguins','Penguins'),(101,4,'Atlanta Trashers','Trashers'),(102,4,'Carolina Hurricanes','Hurricanes'),(103,4,'Florida Panthers','Panthers'),(104,4,'Tampa Bay Lightnings','Lightnings'),(105,4,'Washington Capitals','Capitals'),(106,4,'Chicago Blackhawks','Blackhawks'),(107,4,'Columbo Blue Jackets','Blue Jackets'),(108,4,'Detroit Red Wings','Red Wings'),(109,4,'Nashville Predators','Predators'),(110,4,'St.Louis Blues','Blues'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (111,4,'Calgary Flames','Flames'),(112,4,'Colorado Avalanche','Avalanche'),(113,4,'Edmonton Oilers','Oilers'),(114,4,'Minnesota Wild','Wild'),(115,4,'Vancouver Canucks','Canucks'),(116,4,'Anaheim Mighty Ducks','Mighty Ducks'),(117,4,'Dallas Stars','Stars'),(118,4,'Los Angeles Kings','Kings'),(119,4,'Phoenix Coyotes','Coyotes'),(120,4,'San Jose Sharks','Sharks'),(121,1,'Houston Texans','Texans'),(122,1,'Houston Oilers','Oilers'); +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; diff --git a/springboot/docker-compose.yml b/springboot/docker-compose.yml index 0adf164..fc4e875 100644 --- a/springboot/docker-compose.yml +++ b/springboot/docker-compose.yml @@ -11,21 +11,21 @@ services: ports: - 3316:3306 networks: - - back-end + - database volumes: - mariadb-storage:/var/lib/mysql:rw kontor: image: kontor restart: unless-stopped networks: - - back-end - - front-end + - database + - frontend ports: - 8000:8000 networks: - back-end: - front-end: + database: + frontend: volumes: mariadb-storage: