mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
Windows: Automatically resolve shortcuts (.lnk files) when adding books to calibre. Fixes #1907410 [Enhancement request: Dragging a shortcut file into Calibre should add target file](https://bugs.launchpad.net/calibre/+bug/1907410)
This commit is contained in:
parent
8b4dae7c9e
commit
74f7adb510
@ -7,26 +7,27 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os
|
||||
from functools import partial
|
||||
from collections import defaultdict
|
||||
|
||||
from PyQt5.Qt import QPixmap, QTimer, QApplication, QDialog
|
||||
from functools import partial
|
||||
from PyQt5.Qt import QApplication, QDialog, QPixmap, QTimer
|
||||
|
||||
from calibre import as_unicode, guess_type
|
||||
from calibre.gui2 import (error_dialog, choose_files, choose_dir,
|
||||
warning_dialog, info_dialog, gprefs)
|
||||
from calibre.constants import iswindows
|
||||
from calibre.ebooks import BOOK_EXTENSIONS
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
from calibre.gui2 import (
|
||||
choose_dir, choose_files, error_dialog, gprefs, info_dialog, question_dialog,
|
||||
warning_dialog
|
||||
)
|
||||
from calibre.gui2.actions import InterfaceAction
|
||||
from calibre.gui2.dialogs.add_empty_book import AddEmptyBookDialog
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.gui2.dialogs.progress import ProgressDialog
|
||||
from calibre.ebooks import BOOK_EXTENSIONS
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from calibre.utils.config_base import tweaks
|
||||
from calibre.utils.filenames import ascii_filename, make_long_path_useable
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.gui2.actions import InterfaceAction
|
||||
from calibre.gui2 import question_dialog
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from polyglot.builtins import iteritems, string_or_bytes, range
|
||||
from polyglot.builtins import iteritems, range, string_or_bytes
|
||||
|
||||
|
||||
def get_filters():
|
||||
@ -430,6 +431,9 @@ class AddAction(InterfaceAction):
|
||||
formats = []
|
||||
from calibre.gui2.dnd import image_extensions
|
||||
image_exts = set(image_extensions()) - set(tweaks['cover_drop_exclude'])
|
||||
if iswindows:
|
||||
from calibre.gui2.add import resolve_windows_links
|
||||
paths = list(resolve_windows_links(paths, hwnd=int(self.gui.effectiveWinId())))
|
||||
for path in paths:
|
||||
ext = os.path.splitext(path)[1].lower()
|
||||
if ext:
|
||||
|
@ -7,6 +7,7 @@ __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import traceback
|
||||
@ -61,6 +62,24 @@ def validate_source(source, parent=None): # {{{
|
||||
# }}}
|
||||
|
||||
|
||||
def resolve_windows_links(paths, hwnd=None):
|
||||
try:
|
||||
from calibre_extensions.winutil import resolve_lnk
|
||||
except ImportError:
|
||||
return paths
|
||||
for x in paths:
|
||||
if x.lower().endswith('.lnk'):
|
||||
try:
|
||||
if hwnd is None:
|
||||
x = resolve_lnk(x)
|
||||
else:
|
||||
x = resolve_lnk(x, 0, hwnd)
|
||||
except Exception as e:
|
||||
print('Failed to resolve link', x, 'with error:', e, file=sys.stderr)
|
||||
continue
|
||||
yield x
|
||||
|
||||
|
||||
class Adder(QObject):
|
||||
|
||||
do_one_signal = pyqtSignal()
|
||||
@ -85,6 +104,11 @@ class Adder(QObject):
|
||||
self.do_one_signal.connect(self.tick, type=Qt.ConnectionType.QueuedConnection)
|
||||
self.pool = pool
|
||||
self.pd = ProgressDialog(_('Adding books...'), _('Scanning for files...'), min=0, max=0, parent=parent, icon='add_book.png')
|
||||
self.win_id = None
|
||||
if parent is not None and hasattr(parent, 'effectiveWinId'):
|
||||
self.win_id = parent.effectiveWinId()
|
||||
if self.win_id is not None:
|
||||
self.win_id = int(self.win_id)
|
||||
self.db = getattr(db, 'new_api', None)
|
||||
if self.db is not None:
|
||||
self.dbref = weakref.ref(db)
|
||||
@ -149,6 +173,9 @@ class Adder(QObject):
|
||||
for files in find_books_in_directory(dirpath, self.single_book_per_directory, compiled_rules=compiled_rules):
|
||||
if self.abort_scan:
|
||||
return
|
||||
if iswindows:
|
||||
files = list(resolve_windows_links(files, hwnd=self.win_id))
|
||||
if files:
|
||||
self.file_groups[len(self.file_groups)] = files
|
||||
else:
|
||||
def find_files(root):
|
||||
@ -195,7 +222,13 @@ class Adder(QObject):
|
||||
find_files(extract(path))
|
||||
self.ignore_opf = True
|
||||
else:
|
||||
self.file_groups[len(self.file_groups)] = [path]
|
||||
x = [path]
|
||||
if iswindows:
|
||||
x = list(resolve_windows_links(x, hwnd=self.win_id))
|
||||
if x:
|
||||
self.file_groups[len(self.file_groups)] = x
|
||||
else:
|
||||
unreadable_files.append(path)
|
||||
else:
|
||||
unreadable_files.append(path)
|
||||
if unreadable_files:
|
||||
|
@ -20,6 +20,7 @@ from PyQt5.Qt import (
|
||||
)
|
||||
|
||||
from calibre import strftime
|
||||
from calibre.constants import iswindows
|
||||
from calibre.customize.ui import run_plugins_on_import
|
||||
from calibre.db import SPOOL_SIZE
|
||||
from calibre.ebooks import BOOK_EXTENSIONS
|
||||
@ -973,6 +974,11 @@ class FormatsManager(QWidget):
|
||||
bad_perms = []
|
||||
for _file in paths:
|
||||
_file = make_long_path_useable(os.path.abspath(_file))
|
||||
if iswindows:
|
||||
from calibre.gui2.add import resolve_windows_links
|
||||
x = list(resolve_windows_links([_file], hwnd=int(self.effectiveWinId())))
|
||||
if x:
|
||||
_file = x[0]
|
||||
if not os.access(_file, os.R_OK):
|
||||
bad_perms.append(_file)
|
||||
continue
|
||||
|
@ -11,6 +11,7 @@
|
||||
#include <Windows.h>
|
||||
#include <Python.h>
|
||||
#include <comdef.h>
|
||||
#define arraysz(x) (sizeof(x)/sizeof(x[0]))
|
||||
|
||||
static inline PyObject*
|
||||
set_error_from_hresult(const char *file, const int line, const HRESULT hr, const char *prefix="", PyObject *name=NULL) {
|
||||
|
@ -675,6 +675,44 @@ winutil_move_to_trash(PyObject *self, PyObject *args) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
resolve_lnk(PyObject *self, PyObject *args) {
|
||||
wchar_raii path;
|
||||
HRESULT hr;
|
||||
PyObject *win_id = NULL;
|
||||
unsigned short timeout = 0;
|
||||
if (!PyArg_ParseTuple(args, "O&|HO!", py_to_wchar, &path, &timeout, &PyLong_Type, &win_id)) return NULL;
|
||||
if (!path.ptr()) {
|
||||
PyErr_SetString(PyExc_TypeError, "Path must not be None");
|
||||
return NULL;
|
||||
}
|
||||
scoped_com_initializer com;
|
||||
if (!com.succeded()) { PyErr_SetString(PyExc_OSError, "Failed to initialize COM"); return NULL; }
|
||||
CComPtr<IShellLink> shell_link;
|
||||
if (FAILED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shell_link)))) {
|
||||
PyErr_SetString(PyExc_OSError, "Failed to create IShellLink instance");
|
||||
return NULL;
|
||||
}
|
||||
CComPtr<IPersistFile> persist_file;
|
||||
if (FAILED(shell_link->QueryInterface(IID_PPV_ARGS(&persist_file)))) {
|
||||
PyErr_SetString(PyExc_OSError, "Failed to create IPersistFile instance");
|
||||
return NULL;
|
||||
}
|
||||
hr = persist_file->Load(path.ptr(), 0);
|
||||
if (FAILED(hr)) return error_from_hresult(hr, "Failed to load link");
|
||||
DWORD flags = SLR_UPDATE | ( (timeout & 0xffff) << 16 );
|
||||
if (win_id) {
|
||||
hr = shell_link->Resolve(static_cast<HWND>(PyLong_AsVoidPtr(win_id)), flags);
|
||||
} else {
|
||||
hr = shell_link->Resolve(NULL, flags | SLR_NO_UI | SLR_NOTRACK | SLR_NOLINKINFO);
|
||||
}
|
||||
if (FAILED(hr)) return error_from_hresult(hr, "Failed to resolve link");
|
||||
wchar_t buf[2048];
|
||||
hr = shell_link->GetPath(buf, arraysz(buf), NULL, 0);
|
||||
if (FAILED(hr)) return error_from_hresult(hr, "Failed to get path from link");
|
||||
return PyUnicode_FromWideChar(buf, -1);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
winutil_manage_shortcut(PyObject *self, PyObject *args) {
|
||||
wchar_raii path, target, description, quoted_args;
|
||||
@ -701,7 +739,7 @@ winutil_manage_shortcut(PyObject *self, PyObject *args) {
|
||||
if (!target.ptr()) {
|
||||
wchar_t buf[2048];
|
||||
if (FAILED(persist_file->Load(path.ptr(), 0))) Py_RETURN_NONE;
|
||||
if (FAILED(shell_link->GetPath(buf, sizeof(buf), NULL, 0))) Py_RETURN_NONE;
|
||||
if (FAILED(shell_link->GetPath(buf, arraysz(buf), NULL, 0))) Py_RETURN_NONE;
|
||||
return Py_BuildValue("u", buf);
|
||||
}
|
||||
|
||||
@ -1172,6 +1210,10 @@ static PyMethodDef winutil_methods[] = {
|
||||
"manage_shortcut()\n\nManage a shortcut"
|
||||
},
|
||||
|
||||
{"resolve_lnk", (PyCFunction)resolve_lnk, METH_VARARGS,
|
||||
"resolve_lnk()\n\nGet the target of a lnk file."
|
||||
},
|
||||
|
||||
{"get_file_id", (PyCFunction)winutil_get_file_id, METH_VARARGS,
|
||||
"get_file_id(path)\n\nGet the windows file id (volume_num, file_index_high, file_index_low)"
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user