packages feed

imbib-1.2.5: Batch.hs

{-# LANGUAGE RecordWildCards, TupleSections  #-}

import Data.List
import System.FilePath
import qualified Data.Map as M
import Config
import Options.Applicative

import TypedBibData
import BibDB
import BibAttach
import qualified SuffixTreeCluster as SC
import MaybeIO
import Diff
import Data.Function (on)
-------------------------------------------------------------------------
-- CheckDuplicates, method 1


pairs :: [t] -> [(t, t)]
pairs (x:xs) = map (x,) xs ++ pairs xs
pairs _ = []

dist :: String -> String -> Int
dist x y = length $ filter (/= B) $ map fst $ diff' (project x) (project y)

checkDup :: InitFile -> [Entry] -> MaybeIO ()
checkDup cfg bib' = do
  -- mapM_ putString $  [ groom $ (map findTitle es, shared) | (es,shared) <- common ]
  putString ("Removing exact duplicates:" ++ show (length bib' - length bib))
  -- putString ("Candidate duplicate groups:\n" ++ (groom $ [(shared,map findTitle es) | (es,shared) <- dupGroups]))
  uncheckedHarmless $ mapM_ print [(findTitle e1,findTitle e2) | (e1,e2) <- dups]
  sortBib cfg $ bib -- (uniqDups ++ (bib \\ uniqDups))
  where dups = [(e1,e2) |
                (es,_) <- dupGroups,
                (e1,e2) <- pairs $ es,
                not (areRelated e1 e2), -- no "see also" field
                dist (findTitle e1) (findTitle e2) <= 10 -- edit distance
               ]
        dupGroups = [(es,shared) | (es,shared) <- common,
                     -- a sufficently long substring is shared
                     maximum (map length shared) >= threshold (minimum (map (length . project . findTitle) es))]
        threshold x = (9 * x) `div` 10
        -- SC.printTree $ SC.select ("", clusterTree)
        common = M.toList $ SC.commonStrings info
        -- clusterTree = SC.construct info
        info = [(project $ findTitle e,[e]) | e <- bib]
        bib = nub bib'

-------------------------------------------------------------------------
-- Auto-renaming of attachments

rename :: (String -> String) -> (String,String) -> MaybeIO (String,String) 
rename new (oldfname,typ)
    | oldfname == newfname = bail
    | otherwise = do
  oex <- doesFileExist oldfname
  nex <- doesFileExist newfname
  case (oex,nex) of
    (True,False) -> do renameFile oldfname newfname
                       return (newfname,typ)
    (False,_) -> putString ("old does not exist: " ++ oldfname) >> bail
    (_,True) -> putString  ("new already exists: " ++ newfname) >> bail
    -- (_,True) -> do removeFile oldfname >> return (newfname,typ)
  where newfname = new typ
        bail = return (oldfname,typ)

renamer :: InitFile -> Entry -> MaybeIO Entry
renamer cfg t@Entry{..} = do
  files <- traverse (rename (findAttachName cfg t)) files
  return $ Entry{..}

renameAttachments :: InitFile -> [Entry] -> MaybeIO ()
renameAttachments cfg bib = do 
  bib' <- traverse (renamer cfg) bib
  saveBib cfg bib'

------------------------------------------------------------------------
-- Check for orphans

check :: (String,String) -> MaybeIO () 
check (fname,_typ) = do
  ex <- doesFileExist fname
  if (not ex) then putString $ "missing: " ++ fname else return ()

checker :: Entry -> MaybeIO ()
checker Entry{..} = mapM_ check files

getDirectoryContents' :: FilePath -> MaybeIO [FilePath]
getDirectoryContents' d = map (d </>) <$> getDirectoryContents d

checkAttachments :: InitFile -> [Entry] -> MaybeIO ()
checkAttachments cfg bib = do
  fnames <- getDirectoryContents' (attachmentsRoot cfg)
  let attachments = [ f | e <- bib, (f,_t) <- files e] 

  mapM_ putString (fnames \\ attachments)

----------------------------------------------------------------------
-- Merge another bibtex file

cleanImported :: Entry -> Entry
cleanImported Entry {..} = Entry {files=[],..}

mergeIn :: InitFile -> [Entry] -> String -> MaybeIO ()
mergeIn cfg bib fname = do
  bib2 <- uncheckedHarmless (rightOrDie =<< loadBibliographyFrom fname)
  
  saveBib cfg $ map cleanImported bib2 ++ bib
  return ()


{-
mergeBibs bib1 bib2 =
    where bibM1 = M.fromList [(project $ findTitle e, e) | e <- bib1]
-}


-----------------------------------------------------------------------
-- Harvest Downloads

harvest :: InitFile -> [Entry] -> MaybeIO ()
harvest cfg bib = do
  contents <- getDirectoryContents' (downloadsDirectory cfg)
  let oldFiles = concatMap (map snd . files) bib
      newFiles = contents \\ oldFiles
      papers = [Entry {kind = "download",
                       seeAlso = [],
                       authors = [],
                       files = [(fname,guessTypeByName fname)],
                       otherFields = [("title",fname),("date","2010")]
                      } | fname <- newFiles, takeExtension fname `elem` [".pdf",".ps"]]
  saveBib cfg $ papers ++ bib


------------------------------------------------------------------------
-- Driver

sortBib :: InitFile -> [Entry] -> MaybeIO ()
sortBib cfg b = saveBib cfg (sortBy (compare `on` \x -> (findFirstAuthor x,findYear x,findTitle x)) b)

saveBib :: InitFile -> [Entry] -> MaybeIO ()
saveBib cfg b = safely "Saving bibfile" $ saveBibliography cfg b

withInfo :: Parser a -> String -> ParserInfo a
withInfo opts desc = info (helper <*> opts) $ progDesc desc

parseCommand :: InitFile -> [Entry] -> Parser (MaybeIO ())
parseCommand cfg bib = subparser $
    command "check"   ((pure (checkAttachments cfg bib)                     `withInfo` "check that attachment exist")) <>
    command "rename"  ((pure (renameAttachments cfg bib)                    `withInfo` "rename/move atachments to where they belong")) <>
    command "import"  ((mergeIn cfg bib <$> (argument str (metavar "FILE")) `withInfo` "merge a bibfile into the database")) <>
    command "harvest" ((pure (harvest cfg bib)                              `withInfo` "harvest attachments (???)")) <>
    command "dup"     ((pure (checkDup cfg bib)                             `withInfo` "check for duplicates")) <>
    command "cleanup" ((pure (saveBib cfg bib)                              `withInfo` "cleanup keys etc.")) <>
    command "sort"    ((pure (sortBib cfg bib)                              `withInfo` "sort entries by key"))

main :: IO ()
main = do
  cfg <- loadConfiguration
  bib <- rightOrDie =<< loadBibliography cfg
  let options :: ParserInfo (Bool, MaybeIO ())
      options =
        (((,) <$> 
           switch (short 'd' <> long "dry-run" <> help "don't perform any change (dry run)") <*>
           parseCommand cfg bib
         ) `withInfo` "batch handling of bib db")
  (dry,cmd) <- execParser options
  run dry cmd

dryRun :: MaybeIO a -> IO a
dryRun = run True

trueRun :: MaybeIO a -> IO a
trueRun = run False


-- Local Variables:
-- dante-target: "imbibatch"
-- End: