├── 4chan_gui.py ├── LICENSE ├── README.md ├── icons ├── big_icon.png ├── clear.svg ├── continue.svg ├── download.svg ├── mini_icon.png └── stop.svg └── screenshot.png /4chan_gui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | """ The Chandler: imageboard pictures downloader 4 | This program will download all the images from various imageboards threads until 404'ed """ 5 | 6 | __author__ = "Dhole" 7 | __license__ = "WTFPL - Do What The Fuck You Want To Public License " 8 | __version__ = "0.6" 9 | __email__ = "bankukur@gmail.com" 10 | __status__ = "Beta" 11 | __date__ = "10 May 2012" 12 | 13 | 14 | from PyQt4.QtCore import * 15 | from PyQt4.QtGui import * 16 | from urllib.request import urlopen, urlretrieve 17 | from time import sleep 18 | from contextlib import closing 19 | import urllib.error 20 | import sys, operator, pickle, os, threading, re, webbrowser, shutil, queue, urllib.request, platform, subprocess 21 | 22 | import socket 23 | socket.setdefaulttimeout(10) 24 | 25 | 26 | class Glob(object): 27 | 28 | stop = False 29 | update = False 30 | my_array = [] 31 | header = ['url', 'im board', 'section', 'thread', 'images', 'status'] 32 | x = {} 33 | threadLock_mem = threading.RLock() 34 | threadLock_file = threading.RLock() 35 | db = '' 36 | q = {} 37 | 38 | def initialize(db): 39 | 40 | #Check db 41 | Glob.threadLock_mem.acquire() 42 | if not os.path.exists(db): 43 | print("Creating new urls_file") 44 | x = {} 45 | with open(db, 'wb') as f: 46 | pickle.dump(x, f) 47 | else: 48 | with open(db, 'rb') as f: 49 | try: 50 | x = pickle.load(f) 51 | except EOFError: 52 | print("Error opening file " + db + ". Exiting...") 53 | sys.exit(1) 54 | Glob.x = x 55 | print("initializing..." + db) 56 | Glob.update_values() 57 | Glob.threadLock_mem.release() 58 | Glob.db = db 59 | 60 | def write(): 61 | Glob.threadLock_mem.acquire() 62 | Glob.threadLock_file.acquire() 63 | with open(Glob.db, 'wb') as f: 64 | pickle.dump(Glob.x, f) 65 | Glob.threadLock_file.release() 66 | Glob.threadLock_mem.release() 67 | 68 | def update_values(): 69 | Glob.threadLock_mem.acquire() 70 | i = 0 71 | my_array = [] 72 | for k,v in Glob.x.items(): 73 | my_array.append([]) 74 | my_array[i].append(k) 75 | my_array[i].append(v['imboard']) 76 | my_array[i].append(v['section']) 77 | my_array[i].append(v['thread']) 78 | my_array[i].append(v['number_images']) 79 | 80 | if v['is404']: 81 | my_array[i].append('404') 82 | else: 83 | if v['isPaused']: 84 | my_array[i].append('Paused') 85 | else: 86 | my_array[i].append('Active') 87 | i = i + 1 88 | if len(Glob.x) == 0: 89 | my_array.append([[' '],[' '],[' '],[' '],[' '],[' ']]) 90 | 91 | Glob.threadLock_mem.release() 92 | Glob.my_array = my_array 93 | 94 | def delete(url): 95 | Glob.threadLock_mem.acquire() 96 | del Glob.x[url] 97 | Glob.write() 98 | Glob.threadLock_mem.release() 99 | imboard = get_imageboard(url) 100 | section = get_section(url) #re.findall("4chan.org/[a-z0-9]*/res", url)[0].split("/")[1] 101 | number = get_number_thread(url) #re.findall("res/[0-9]*", url)[0][4:] 102 | path = os.getcwd() 103 | path = os.path.join(path, imboard) 104 | path = os.path.join(path, section) 105 | path = os.path.join(path, number) 106 | if os.path.isdir(path): 107 | shutil.rmtree(path) 108 | else: 109 | print('Folder didn\'t exist') 110 | 111 | class TheTable(QTableView): 112 | def __init__(self, parent = None): 113 | QTableView.__init__(self, parent) 114 | 115 | self.tablemodel = MyTableModel(Glob.my_array, Glob.header, self) 116 | 117 | self.setModel(self.tablemodel) 118 | self.resizeRowsToContents() 119 | self.resizeColumnsToContents() 120 | self.setSortingEnabled(True) 121 | self.verticalHeader().hide() 122 | for i in range(0,6): 123 | cur_size = self.horizontalHeader().sectionSize(i) 124 | self.horizontalHeader().resizeSection(i,cur_size + 20) 125 | self.horizontalHeader().setResizeMode(QHeaderView.Fixed) 126 | self.horizontalHeader().setStretchLastSection(True) 127 | self.current_row = -1 128 | 129 | def mousePressEvent(self, event): 130 | index = self.indexAt(event.pos()) 131 | 132 | if (index.isValid()): 133 | self.current_row = row = index.row() 134 | self.selectRow(self.current_row) 135 | else: 136 | self.current_row = row = -1 137 | self.clearSelection() 138 | 139 | def contextMenuEvent(self, event): 140 | 141 | index = self.indexAt(event.pos()) 142 | 143 | if (index.isValid() and len(Glob.x) != 0): 144 | row = index.row() 145 | self.current_row = row 146 | self.selectRow(self.current_row) 147 | column = index.column() 148 | value = self.model().index(row, 0).data() 149 | else: 150 | self.current_row = -1 151 | return 152 | 153 | # The menu 154 | menu = QMenu(self) 155 | # Add your actions 156 | continueAction = QAction("Continue", self) 157 | pauseAction = QAction("Pause", self) 158 | browse_urlAction = QAction("Open in broswer", self) 159 | copyAction = QAction("Copy url", self) 160 | clearAction = QAction("Clear", self) 161 | view_folderAction = QAction("View folder", self) 162 | deleteAction = QAction("Delete", self) 163 | 164 | menu.addAction(browse_urlAction) 165 | menu.addAction(view_folderAction) 166 | menu.addAction(copyAction) 167 | menu.addSeparator() 168 | menu.addAction(pauseAction) 169 | menu.addAction(continueAction) 170 | menu.addSeparator() 171 | menu.addAction(clearAction) 172 | menu.addAction(deleteAction) 173 | 174 | continueAction.triggered.connect(lambda: self.continue_slot(value)) 175 | pauseAction.triggered.connect(lambda: self.pause_slot(value)) 176 | browse_urlAction.triggered.connect(lambda: self.browse_url_slot(value)) 177 | copyAction.triggered.connect(lambda: self.copy_slot(value)) 178 | clearAction.triggered.connect(lambda: self.clear_slot(value)) 179 | deleteAction.triggered.connect(lambda: self.delete_slot(value)) 180 | view_folderAction.triggered.connect(lambda: self.view_folder_slot(value)) 181 | 182 | #menu.addAction(pauseAction) 183 | #menu.addAction(clearAction) 184 | #menu.addAction(delete_filesAction) 185 | #menu.addAction(view_folderAction) 186 | self.clipboard = QApplication.clipboard() 187 | menu.popup(event.globalPos()) 188 | 189 | def continue_slot(self, value): 190 | print("Continuing with " + value) 191 | Glob.q[value].put('continue') 192 | 193 | def pause_slot(self, value): 194 | print("Pausing " + value) 195 | Glob.q[value].put('pause') 196 | 197 | def browse_url_slot(self, value): 198 | print("Browsing url " + value) 199 | webbrowser.open(value) 200 | 201 | def copy_slot(self, value): 202 | print("Copying " + value) 203 | self.clipboard.setText(value) 204 | 205 | def clear_slot(self, value): 206 | print("Clearing " + value) 207 | Glob.threadLock_mem.acquire() 208 | if Glob.x[value]['is404']: 209 | del Glob.x[value] 210 | Glob.write() 211 | else: 212 | print(value + " is not 404, you should delete it instead") 213 | Glob.threadLock_mem.release() 214 | 215 | def view_folder_slot(self, value): 216 | print("Viewing folder" + value) 217 | imboard = get_imageboard(value) 218 | section = get_section(value) #re.findall("4chan.org/[a-z0-9]*/res", value)[0].split("/")[1] 219 | number = get_number_thread(value) #re.findall("res/[0-9]*", value)[0][4:] 220 | path = os.getcwd() 221 | path = os.path.join(path, imboard) 222 | path = os.path.join(path, section) 223 | path = os.path.join(path, number) 224 | #This works fine on unix 225 | if platform.system() == 'Windows': 226 | subprocess.Popen('explorer \"'+path+'\"') 227 | else: 228 | os.system("xdg-open " + path) 229 | 230 | def delete_slot(self, value): 231 | print("Deleting " + value) 232 | Glob.threadLock_mem.acquire() 233 | is404 = Glob.x[value]['is404'] 234 | Glob.threadLock_mem.release() 235 | if is404: 236 | Glob.delete(value) 237 | else: 238 | Glob.q[value].put('delete') 239 | 240 | 241 | #def updateGeometries(self): 242 | #super(TheTable, self).updateGeometries() 243 | #self.verticalScrollBar().setSingleStep(2) 244 | 245 | 246 | class MyWindow(QMainWindow): 247 | def __init__(self, *args): 248 | QWidget.__init__(self, *args) 249 | 250 | self.centralwidget = QWidget(self) 251 | topBox = QHBoxLayout() 252 | box = QVBoxLayout(self.centralwidget) 253 | self.urlLine = QLineEdit(self) 254 | topBox.addWidget(self.urlLine) 255 | self.downloadButton = QPushButton('', self) 256 | self.downloadButton.setIcon(QIcon('icons/download.svg')) 257 | self.downloadButton.setEnabled(False) 258 | topBox.addWidget(self.downloadButton) 259 | #self.downloadMoreButton = QPushButton('Download more', self) 260 | #topBox.addWidget(self.downloadMoreButton) 261 | box.addLayout(topBox) 262 | self.tb = TheTable(self.centralwidget) 263 | box.addWidget(self.tb) 264 | self.setCentralWidget(self.centralwidget) 265 | 266 | self.url = '' 267 | self.downloadButton.clicked.connect(self.downloadUrl) 268 | self.urlLine.textChanged[str].connect(self.enteredUrl) 269 | self.urlLine.returnPressed.connect(self.downloadUrl) 270 | 271 | self.createActions() 272 | self.createMenus() 273 | self.createStatusBar() 274 | 275 | pauseAllAction = QAction(QIcon('icons/stop.svg'), 'Pause all', self) 276 | pauseAllAction.setShortcut('Ctrl+P') 277 | pauseAllAction.triggered.connect(self.pauseAllSlot) 278 | 279 | continueAllAction = QAction(QIcon('icons/continue.svg'), 'Continue all', self) 280 | continueAllAction.setShortcut('Ctrl+P') 281 | continueAllAction.triggered.connect(self.continueAllSlot) 282 | 283 | clear404Action = QAction(QIcon('icons/clear.svg'), 'Clear all 404', self) 284 | clear404Action.setShortcut('Ctrl+P') 285 | clear404Action.triggered.connect(self.clear404Slot) 286 | 287 | self.menu = QMenu() 288 | self.menu.addAction("un") 289 | self.menu.addAction("dos") 290 | 291 | self.toolbar = self.addToolBar('Pause all') 292 | self.toolbar.setFloatable(False) 293 | self.toolbar.setMovable(False) 294 | self.toolbar.addAction(pauseAllAction) 295 | self.toolbar = self.addToolBar('Continue all') 296 | self.toolbar.setFloatable(False) 297 | self.toolbar.setMovable(False) 298 | self.toolbar.addAction(continueAllAction) 299 | self.toolbar = self.addToolBar('Clear all 404 all') 300 | self.toolbar.setFloatable(False) 301 | self.toolbar.setMovable(False) 302 | self.toolbar.addAction(clear404Action) 303 | 304 | self.setWindowIcon(QIcon('icons/big_icon.png')) 305 | self.setGeometry(300, 50, 800, 400) 306 | self.update_dimensions() 307 | self.setWindowTitle('The Chandler') 308 | self.show() 309 | 310 | def update_dimensions(self): 311 | self.tb.resizeRowsToContents() 312 | self.tb.resizeColumnsToContents() 313 | width = 0 314 | 315 | for i in range(0,6): 316 | cur_size = self.tb.horizontalHeader().sectionSize(i) 317 | self.tb.horizontalHeader().resizeSection(i,cur_size) 318 | width = width + cur_size + 8 319 | 320 | if width < 600: 321 | width = 600 322 | 323 | self.setMaximumWidth(width) 324 | self.setMinimumWidth(width/2) 325 | self.setMinimumHeight(400) 326 | self.tb.horizontalHeader().setStretchLastSection(True) 327 | 328 | def enteredUrl(self, line): 329 | if line == '': 330 | self.downloadButton.setEnabled(False) 331 | else: 332 | self.downloadButton.setEnabled(True) 333 | self.url = line 334 | 335 | def downloadUrl(self): 336 | url_ok = check_url(self.url) 337 | if url_ok == "": 338 | print("Bad url!") 339 | else: 340 | print("Downloading url " + url_ok) 341 | self.urlLine.clear() 342 | add_db(url_ok) 343 | Glob.update = True 344 | Glob.update_values() 345 | self.update_table() 346 | 347 | def update_table(self): 348 | Glob.update_values() 349 | 350 | self.tb.tablemodel.update(Glob.my_array) # = MyTableModel(Glob.my_array, Glob.header, self.tb) 351 | #self.tb.show() 352 | 353 | self.tb.setModel(self.tb.tablemodel) 354 | sort_column = self.tb.horizontalHeader().sortIndicatorSection() 355 | sort_order = self.tb.horizontalHeader().sortIndicatorOrder() 356 | self.tb.sortByColumn(sort_column, sort_order) 357 | self.update_dimensions() 358 | if self.tb.current_row != -1: 359 | self.tb.selectRow(self.tb.current_row) 360 | 361 | def pauseAllSlot(self): 362 | self.statusBar().showMessage(self.tr("Pausing all threads")) 363 | print("Pausing all threads") 364 | for k, v in Glob.q.items(): 365 | Glob.q[k].put('pause') 366 | 367 | def continueAllSlot(self): 368 | self.statusBar().showMessage(self.tr("Continuing all threads")) 369 | print("Continuing all threads") 370 | for k, v in Glob.q.items(): 371 | Glob.q[k].put('continue') 372 | 373 | def clear404Slot(self): 374 | self.statusBar().showMessage(self.tr("Clearing all 404 threads")) 375 | Glob.threadLock_mem.acquire() 376 | to_delete = [] 377 | for k, v in Glob.x.items(): 378 | if v['is404']: 379 | to_delete.append(k) 380 | for k in to_delete: 381 | del Glob.x[k] 382 | Glob.write() 383 | Glob.threadLock_mem.release() 384 | 385 | def about(self): 386 | QMessageBox.about(self, self.tr("About The Chandler"), 387 | self.tr("The Chandler\n\n" 388 | "by %s\n" 389 | "version %s\n" 390 | "%s" % (__author__, __version__, __date__))) 391 | 392 | def createActions(self): 393 | self.exitAct = QAction(self.tr("E&xit"), self) 394 | self.exitAct.setShortcut(self.tr("Ctrl+Q")) 395 | self.exitAct.setStatusTip(self.tr("Exit the application")) 396 | self.connect(self.exitAct, SIGNAL("triggered()"), self, SLOT("close()")) 397 | 398 | self.aboutAct = QAction(self.tr("&About"), self) 399 | self.aboutAct.setStatusTip(self.tr("Show the application's About box")) 400 | self.connect(self.aboutAct, SIGNAL("triggered()"), self.about) 401 | 402 | self.aboutQtAct = QAction(self.tr("About &Qt"), self) 403 | self.aboutQtAct.setStatusTip(self.tr("Show the Qt library's About box")) 404 | self.connect(self.aboutQtAct, SIGNAL("triggered()"), qApp, SLOT("aboutQt()")) 405 | 406 | def createMenus(self): 407 | self.fileMenu = self.menuBar().addMenu(self.tr("&File")) 408 | self.fileMenu.addAction(self.exitAct) 409 | 410 | self.helpMenu = self.menuBar().addMenu(self.tr("&Help")) 411 | self.helpMenu.addAction(self.aboutAct) 412 | self.helpMenu.addAction(self.aboutQtAct) 413 | 414 | def createStatusBar(self): 415 | sb = QStatusBar() 416 | sb.setFixedHeight(18) 417 | self.setStatusBar(sb) 418 | self.statusBar().showMessage(self.tr("Ready")) 419 | 420 | 421 | class MyTableModel(QAbstractTableModel): 422 | def __init__(self, datain, headerdata, parent=None, *args): 423 | QAbstractTableModel.__init__(self, parent, *args) 424 | self.arraydata = datain 425 | self.headerdata = headerdata 426 | 427 | def update(self, datain): 428 | self.arraydata = datain 429 | 430 | def rowCount(self, parent): 431 | return len(self.arraydata) 432 | 433 | def columnCount(self, parent): 434 | return len(self.arraydata[0]) 435 | 436 | def data(self, index, role): 437 | if not index.isValid(): 438 | return None 439 | elif role == Qt.BackgroundColorRole : 440 | if self.arraydata[index.row()][5] == "Active": 441 | return QColor(Qt.green) 442 | elif self.arraydata[index.row()][5] == "Paused": 443 | return QColor(Qt.yellow) 444 | elif self.arraydata[index.row()][5] == "404": 445 | return QColor(Qt.gray) 446 | elif role == Qt.DisplayRole: 447 | return self.arraydata[index.row()][index.column()] 448 | return None 449 | 450 | def headerData(self, col, orientation, role): 451 | if orientation == Qt.Horizontal and role == Qt.DisplayRole: 452 | return self.headerdata[col] 453 | return None 454 | 455 | def sort(self, Ncol, order): 456 | """Sort table by given column number. 457 | """ 458 | self.emit(SIGNAL("layoutAboutToBeChanged()")) 459 | self.arraydata = sorted(self.arraydata, key=operator.itemgetter(Ncol)) 460 | if order == Qt.DescendingOrder: 461 | self.arraydata.reverse() 462 | self.emit(SIGNAL("layoutChanged()")) 463 | 464 | 465 | def check_url(url): 466 | #Test if url is ok 467 | if is4chan(url): 468 | return url 469 | url_parsed = re.findall("http(?:s)?://(?:boards.)?.*/*/res/[0-9]*(?:.php|.html)?", url) 470 | if len(url_parsed) < 1: 471 | return "" 472 | else: 473 | return url_parsed[0] 474 | 475 | def is4chan(url): 476 | return "4chan" in url 477 | 478 | def get_section(url): 479 | if is4chan(url): 480 | result = re.findall("4chan.org/.*/thread", url)[0].split("/")[-2] 481 | else: 482 | result = re.findall(".*/[a-z0-9]*/res", url)[0].split("/")[-2] 483 | return result 484 | 485 | def get_number_thread(url): 486 | if is4chan(url): 487 | result = re.findall("(?<=thread/).*", url)[0].replace("/", "_") 488 | else: 489 | result = re.findall("res/[0-9]*", url)[0][4:] 490 | return result 491 | 492 | def get_imageboard(url): 493 | if is4chan(url): 494 | return "4chan" 495 | else: 496 | result = re.findall(".*/*/res/[0-9]*(?:.php|.html)?", url)[0].split("/")[-4].replace('boards.','').split(".")[0] 497 | return result 498 | 499 | def get_image_urls(url): 500 | 501 | #print('LALALA ' + url) 502 | #fetch html from url 503 | with closing(urlopen(url)) as page: 504 | html_code = page.read() 505 | 506 | if Glob.stop: 507 | sys.exit(1) 508 | 509 | html_code = str(html_code) 510 | #Find urls to the images 511 | if is4chan(url): 512 | images = re.findall('\"[^\"]*/i.4cdn.org/./[0-9]*.(?:jpg|png|gif)\"', html_code) 513 | else: 514 | images = re.findall('\"[^\"]*/src/[0-9]*.(?:jpg|png|gif)\"', html_code) 515 | #Delete duplicate entries 516 | images = list(set(images)) 517 | 518 | images_http = [] 519 | 520 | for im in images: 521 | ima = im.replace('\"', '') 522 | if ima[:4] == 'http': 523 | images_http.append(ima) 524 | elif ima[:2] == '//': 525 | if url[:5] == 'https': 526 | images_http.append('https:'+ima) 527 | else: 528 | images_http.append('http:'+ima) 529 | else: 530 | if url[:5] == 'https': 531 | images_http.append('https://'+url.split('/')[2]+ima) 532 | else: 533 | images_http.append('http://'+url.split('/')[2]+ima) 534 | 535 | return images_http 536 | 537 | def get_image(url): 538 | 539 | #Test if url is up 540 | while True: 541 | try: 542 | with closing(urlopen(url)) as connection: 543 | pass 544 | except urllib.error.HTTPError as e: 545 | if e.getcode() == 404: 546 | print("Url down or something wrong: " + str(e.getcode())) 547 | return '404' 548 | pass 549 | else: 550 | break 551 | 552 | print("Connection problems, trying again in 30 seconds...") 553 | try: 554 | order = Glob.q[url].get(block=True, timeout=30) 555 | except queue.Empty as e: 556 | pass 557 | else: 558 | if order == 'exit': 559 | return 'exit' 560 | else: 561 | break 562 | 563 | #del connection 564 | 565 | 566 | imboard = get_imageboard(url) 567 | section = get_section(url)#re.findall("4chan.org/[a-z0-9]*/res", url)[0].split("/")[1] 568 | number = get_number_thread(url) #re.findall("res/[0-9]*", url)[0][4:] 569 | path = os.getcwd() 570 | 571 | path = os.path.join(path, imboard) 572 | #Create imageboard directory 573 | if not os.path.isdir(path): 574 | os.mkdir(path) 575 | 576 | path = os.path.join(path, section) 577 | #Create section directory 578 | if not os.path.isdir(path): 579 | os.mkdir(path) 580 | 581 | path = os.path.join(path, number) 582 | #Create thread directory 583 | if not os.path.isdir(path): 584 | os.mkdir(path) 585 | 586 | #Download images 587 | down_images = [] 588 | Glob.q[url].put('continue') 589 | while True: 590 | while True: 591 | try: 592 | order = Glob.q[url].get(block=True, timeout=30) 593 | except queue.Empty as e: 594 | break 595 | else: 596 | if order == 'pause': 597 | Glob.threadLock_mem.acquire() 598 | Glob.x[url]['isPaused'] = True 599 | Glob.threadLock_mem.release() 600 | continue 601 | elif order == 'continue': 602 | Glob.threadLock_mem.acquire() 603 | Glob.x[url]['isPaused'] = False 604 | Glob.threadLock_mem.release() 605 | break 606 | elif order == 'delete': 607 | return 'delete' 608 | elif order == 'exit': 609 | return 'exit' 610 | 611 | try: 612 | images = get_image_urls(url) 613 | except urllib.error.HTTPError as e: 614 | if e.getcode() == 404: 615 | print("Url down or something wrong: " + str(e.getcode())) 616 | return '404' 617 | continue 618 | except: 619 | print("Connection problems, trying again in 30 seconds!...") 620 | continue 621 | 622 | for im in images: 623 | if im not in down_images: 624 | if not Glob.q[url].empty(): break 625 | filename = re.findall("[0-9]*.(?:jpg|gif|png)",im)[0] 626 | if not os.path.exists(os.path.join(path,filename)): 627 | try: 628 | urlretrieve(im, os.path.join(path,filename)) 629 | except IOError as e: 630 | print('Network problem') 631 | break 632 | except: 633 | print('Other problem') 634 | break 635 | down_images.append(im) 636 | 637 | Glob.threadLock_mem.acquire() 638 | Glob.x[url]['number_images'] = str(len(down_images)) + "/" + str(len(images)) 639 | Glob.threadLock_mem.release() 640 | 641 | 642 | class Worker(threading.Thread): 643 | def __init__(self, url): 644 | threading.Thread.__init__(self) 645 | self.url = url 646 | 647 | def run(self): 648 | 649 | #Start function to download images 650 | status = get_image(self.url) 651 | 652 | Glob.threadLock_mem.acquire() 653 | if status == '404': 654 | Glob.x[self.url]["is404"] = True 655 | Glob.x[self.url]["isActive"] = False 656 | elif status == 'delete': 657 | Glob.x[self.url]['isPaused'] = True 658 | Glob.write() 659 | Glob.threadLock_mem.release() 660 | if status == 'delete': 661 | Glob.delete(self.url) 662 | 663 | 664 | class Reader(threading.Thread): 665 | def __init__(self): 666 | threading.Thread.__init__(self) 667 | 668 | def run(self): 669 | print("Starting reader...") 670 | Glob.threadLock_mem.acquire() 671 | for k, v in Glob.x.items(): 672 | if Glob.x[k]['is404'] == False: 673 | w = Worker(k) 674 | w.start() 675 | Glob.x[k]["isActive"] = True 676 | Glob.q[k] = queue.Queue() 677 | 678 | Glob.write() 679 | Glob.threadLock_mem.release() 680 | 681 | while True: 682 | if Glob.stop: 683 | sys.exit(1) 684 | elif Glob.update: 685 | Glob.threadLock_mem.acquire() 686 | for k, v in Glob.x.items(): 687 | if not Glob.x[k]["isActive"] and not Glob.x[k]["is404"]: 688 | w = Worker(k) 689 | w.start() 690 | Glob.x[k]["isActive"] = True 691 | Glob.q[k] = queue.Queue() 692 | Glob.write() 693 | Glob.update = False 694 | Glob.threadLock_mem.release() 695 | else: 696 | sleep(1) 697 | 698 | 699 | def print_db(): 700 | print("Database " + Glob.db) 701 | Glob.threadLock_mem.acquire() 702 | print(Glob.x) 703 | Glob.threadLock_mem.release() 704 | 705 | 706 | def add_db(url): 707 | Glob.threadLock_mem.acquire() 708 | if not url in Glob.x: 709 | print("Adding url... " + url) 710 | Glob.x[url] = {} 711 | Glob.x[url]["is404"] = False 712 | Glob.x[url]["isActive"] = False 713 | Glob.x[url]["isPaused"] = False 714 | Glob.x[url]['imboard'] = get_imageboard(url) 715 | Glob.x[url]['section'] = get_section(url)#re.findall("4chan.org/[a-z0-9]*/res", url)[0].split("/")[1] 716 | Glob.x[url]['thread'] = get_number_thread(url)#re.findall("res/[0-9]*", url)[0][4:] 717 | Glob.x[url]['number_images'] = '*/*' 718 | else: 719 | print("Already downloading thread " + url) 720 | 721 | Glob.write() 722 | Glob.threadLock_mem.release() 723 | 724 | 725 | def exit_all(app): 726 | app.exec_() 727 | Glob.stop = True 728 | for k, v in Glob.q.items(): 729 | Glob.q[k].put('exit') 730 | Glob.threadLock_mem.acquire() 731 | Glob.write() 732 | Glob.threadLock_mem.release() 733 | print("Please, wait while the program is finishing") 734 | 735 | 736 | def main(): 737 | 738 | global db 739 | #Check input arguments 740 | if len(sys.argv) != 2: 741 | #print("usage: ./4chan.py urls_file") 742 | #sys.exit(1) 743 | db = 'urls_db' 744 | else: 745 | db = sys.argv[1] 746 | if sys.argv[1] == "s": 747 | db = "urls_db" 748 | Glob.initialize(db) 749 | print_db() 750 | sys.exit(1) 751 | 752 | Glob.initialize(db) 753 | 754 | reader = Reader() 755 | reader.start() 756 | 757 | app = QApplication(sys.argv) 758 | w = MyWindow() 759 | 760 | timer = QTimer() 761 | timer.timeout.connect(w.update_table) 762 | timer.start(1000) 763 | 764 | sys.exit(exit_all(app)) 765 | 766 | if __name__ == "__main__": 767 | main() 768 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##The Chandler [4chan issue FIXED!] 2 | ### Python program to download pictures from multiple imageboard threads like 4chan, 420chan or 7chan. 3 | ### GUI made with PyQt 4 | #### Beta software 5 | ============== 6 | 7 | ## Screenshot 8 | ![ScreenShot](https://raw.github.com/Dhole/4chan-image-dl/master/screenshot.png) 9 | 10 | ## Features 11 | * Allows downloading multiple image threads at the same time, until it reaches 404 12 | * Able to pause/continue downloads 13 | * Able to delete thread folders 14 | * Options available: copy url, open in browser, open in file explorer, continue, pause, clear, delete folder 15 | * Urls and state is saved when the program is closed and recovered once opened 16 | 17 | ## Installation 18 | * To use the program you will need python3.2 and and pyqt4 bindings for python3 19 | * For ubuntu, just do: "sudo apt-get install python3.2 python3-pyqt4" 20 | 21 | * For windows, install python3.2: 22 | * (32 bits) http://www.python.org/ftp/python/3.2/python-3.2.msi 23 | * (64 bits) http://www.python.org/ftp/python/3.2/python-3.2.amd64.msi 24 | * and pyqt4 25 | * (32 bits) http://www.riverbankcomputing.com/static/Downloads/PyQt4/PyQt-Py3.2-x86-gpl-4.9.1-1.exe 26 | * (64 bits) http://www.riverbankcomputing.com/static/Downloads/PyQt4/PyQt-Py3.2-x64-gpl-4.9.1-1.exe 27 | 28 | ## Usage 29 | * To execute, just run 4chan_gui.py 30 | * Alternatively, you can run it in command line with "python3.2 4chan_gui.py [url_db]" 31 | * Where url_db is the alternative name for the file where urls are saved with their state 32 | 33 | ## License 34 | * WTFPL - Do What The Fuck You Want To Public License 35 | * See LICENSE for more information 36 | 37 | -------------------------------------------------------------------------------- /icons/big_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dhole/4chan-image-dl/519bde87639932dd498bb0927d5993683e262a3f/icons/big_icon.png -------------------------------------------------------------------------------- /icons/clear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | image/svg+xml 8 | 9 | Gnome Symbolic Icon Theme 10 | 11 | 12 | 13 | Gnome Symbolic Icon Theme 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /icons/continue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | image/svg+xml 8 | 9 | Gnome Symbolic Icon Theme 10 | 11 | 12 | 13 | Gnome Symbolic Icon Theme 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /icons/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | image/svg+xml 8 | 9 | Gnome Symbolic Icon Theme 10 | 11 | 12 | 13 | Gnome Symbolic Icon Theme 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /icons/mini_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dhole/4chan-image-dl/519bde87639932dd498bb0927d5993683e262a3f/icons/mini_icon.png -------------------------------------------------------------------------------- /icons/stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | image/svg+xml 8 | 9 | Gnome Symbolic Icon Theme 10 | 11 | 12 | 13 | Gnome Symbolic Icon Theme 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dhole/4chan-image-dl/519bde87639932dd498bb0927d5993683e262a3f/screenshot.png --------------------------------------------------------------------------------